diff --git a/odilia/src/events/mod.rs b/odilia/src/events/mod.rs index 0a8079a..1063236 100644 --- a/odilia/src/events/mod.rs +++ b/odilia/src/events/mod.rs @@ -1,209 +1,2 @@ mod cache; mod document; -mod object; - -use std::sync::Arc; - -use futures::stream::StreamExt; -use tokio::sync::mpsc::{Receiver, Sender}; -use tokio_util::sync::CancellationToken; - -use crate::state::ScreenReaderState; -use atspi_common::events::Event; -use atspi_common::{Role, ScrollType}; -use odilia_cache::AccessibleExt; -use odilia_cache::Convertable; -use odilia_common::{ - events::{Direction, ScreenReaderEvent}, - result::OdiliaResult, -}; -use ssip_client_async::Priority; - -#[tracing::instrument(level = "debug", skip_all, ret, err)] -pub async fn structural_navigation( - state: &ScreenReaderState, - dir: Direction, - role: Role, -) -> OdiliaResult { - tracing::debug!("Structural nav call begins!"); - let curr = match state.history_item(0) { - Some(acc) => acc.into_accessible(state.atspi.connection()).await?, - None => return Ok(false), - }; - if let Some(next) = curr.get_next(role, dir == Direction::Backward).await? { - let comp = next.to_component().await?; - let texti = next.to_text().await?; - let curr_prim = curr.try_into()?; - let _: bool = comp.grab_focus().await?; - comp.scroll_to(ScrollType::TopLeft).await?; - state.update_accessible(curr_prim); - let _: bool = texti.set_caret_offset(0).await?; - let role = next.get_role().await?; - let len = texti.character_count().await?; - let text = texti.get_text(0, len).await?; - // saying awaits until it is done talking; you may want to spawn a task - state.say(Priority::Text, format!("{text}, {role}")).await; - Ok(true) - } else { - state.say(Priority::Text, format!("No more {role}s")).await; - Ok(true) - } -} - -#[tracing::instrument(level = "debug", skip(state), ret, err)] -pub async fn sr_event( - state: Arc, - mut sr_events: Receiver, - shutdown: CancellationToken, -) -> eyre::Result<()> { - loop { - tokio::select! { - sr_event = sr_events.recv() => { - tracing::debug!("SR Event received"); - match sr_event { - Some(ScreenReaderEvent::StructuralNavigation(dir, role)) => { - if let Err(e) = structural_navigation(&state, dir, role).await { - tracing::debug!(error = %e, "There was an error with the structural navigation call."); - } else { - tracing::debug!("Structural navigation successful!"); - } - }, - Some(ScreenReaderEvent::StopSpeech) => { - tracing::debug!("Stopping speech!"); - state.stop_speech().await; - }, - Some(ScreenReaderEvent::ChangeMode(new_sr_mode)) => { - tracing::debug!("Changing mode to {:?}", new_sr_mode); - if let Ok(mut sr_mode) = state.mode.lock() { - *sr_mode = new_sr_mode; - } - } - _ => { continue; } - }; - continue; - } - () = shutdown.cancelled() => { - tracing::debug!("sr_event cancelled"); - break; - } - } - } - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all)] -pub async fn receive( - state: Arc, - tx: Sender, - shutdown: CancellationToken, -) { - let events = state.atspi.event_stream(); - tokio::pin!(events); - loop { - tokio::select! { - event = events.next() => { - if let Some(Ok(good_event)) = event { - if let Err(e) = tx.send(good_event).await { - tracing::error!(error = %e, "Error sending atspi event"); - } - } else { - tracing::debug!("Event is either None or an Error variant."); - } - continue; - } - () = shutdown.cancelled() => { - tracing::debug!("receive function is done"); - break; - } - } - } -} - -#[tracing::instrument(level = "debug", skip_all)] -pub async fn process( - state: Arc, - mut rx: Receiver, - shutdown: CancellationToken, -) { - loop { - tokio::select! { - event = rx.recv() => { - match event { - Some(good_event) => { - let state_arc = Arc::clone(&state); - tokio::task::spawn( - dispatch_wrapper(state_arc, good_event) - ); - }, - None => { - tracing::debug!("Event was none."); - } - }; - continue; - } - () = shutdown.cancelled() => { - tracing::debug!("process function is done"); - break; - } - } - } -} - -#[tracing::instrument(level = "debug", skip(state))] -async fn dispatch_wrapper(state: Arc, good_event: Event) { - if let Err(e) = dispatch(&state, good_event).await { - tracing::error!(error = %e, "Could not handle event"); - } else { - tracing::debug!("Event handled without error"); - } -} - -#[tracing::instrument(level = "debug", skip(state), ret, err)] -async fn dispatch(state: &ScreenReaderState, event: Event) -> eyre::Result<()> { - // Dispatch based on interface - match &event { - Event::Object(object_event) => { - object::dispatch(state, object_event).await?; - } - Event::Document(document_event) => { - document::dispatch(state, document_event).await?; - } - Event::Cache(cache_event) => cache::dispatch(state, cache_event).await?, - other_event => { - tracing::debug!( - "Ignoring event with unknown interface: {:#?}", - other_event - ); - } - } - state.event_history_update(event); - Ok(()) -} - -#[cfg(test)] -pub mod dispatch_tests { - use crate::ScreenReaderState; - use eyre::Context; - use odilia_common::settings::ApplicationConfig; - use tokio::sync::mpsc::channel; - - #[tokio::test] - async fn test_full_cache() -> eyre::Result<()> { - let state = generate_state().await?; - assert_eq!(state.cache.by_id.len(), 14_738); - Ok(()) - } - - pub async fn generate_state() -> eyre::Result { - let (send, _recv) = channel(32); - let cache = serde_json::from_str(include_str!("wcag_cache_items.json")) - .context("unable to load cache data from json file")?; - let state = ScreenReaderState::new(send, ApplicationConfig::default()) - .await - .context("unable to realise screenreader state")?; - state.cache - .add_all(cache) - .context("unable to add cache to the system")?; - Ok(state) - } -} diff --git a/odilia/src/events/object.rs b/odilia/src/events/object.rs deleted file mode 100644 index c84ceac..0000000 --- a/odilia/src/events/object.rs +++ /dev/null @@ -1,661 +0,0 @@ -use crate::state::ScreenReaderState; -use atspi_common::events::object::ObjectEvents; - -#[tracing::instrument(level = "debug", skip(state), err)] -pub async fn dispatch(state: &ScreenReaderState, event: &ObjectEvents) -> eyre::Result<()> { - // Dispatch based on member - match event { - ObjectEvents::StateChanged(state_changed_event) => { - state_changed::dispatch(state, state_changed_event).await?; - } - ObjectEvents::TextCaretMoved(text_caret_moved_event) => { - text_caret_moved::dispatch(state, text_caret_moved_event).await?; - } - ObjectEvents::TextChanged(text_changed_event) => { - text_changed::dispatch(state, text_changed_event).await?; - } - ObjectEvents::ChildrenChanged(children_changed_event) => { - children_changed::dispatch(state, children_changed_event).await?; - } - other_member => { - tracing::debug!("Ignoring event with unknown member: {:#?}", other_member); - } - } - Ok(()) -} - -mod text_changed { - use crate::state::ScreenReaderState; - use atspi_common::{events::object::TextChangedEvent, Operation}; - use odilia_cache::CacheItem; - use odilia_common::{ - errors::OdiliaError, - result::OdiliaResult, - types::{AriaAtomic, AriaLive}, - }; - use ssip_client_async::Priority; - use std::collections::HashMap; - - #[tracing::instrument(level = "trace")] - pub fn update_string_insert( - start_pos: usize, - update_length: usize, - updated_text: &str, - ) -> impl Fn(&mut CacheItem) + '_ { - move |cache_item| { - tracing::trace!( - "Insert into \"{}\"({:?} @ {}+{} should insert \"{}\"", - cache_item.text, - cache_item.object.id, - start_pos, - update_length, - updated_text - ); - let char_num = cache_item.text.chars().count(); - let prepend = start_pos == 0; - let append = start_pos >= char_num; - // if the end of the inserted string will go past the end of the original string - let insert_and_append = start_pos + update_length >= char_num; - cache_item.text = if prepend { - append_to_object(updated_text, &cache_item.text) - } else if append { - append_to_object(&cache_item.text, updated_text) - } else if insert_and_append { - insert_at_index(&cache_item.text, updated_text, start_pos) - } else { - insert_at_range( - &cache_item.text, - updated_text, - start_pos, - start_pos + update_length, - ) - } - } - } - - pub fn append_to_object(original: &str, to_append: &str) -> String { - let mut new_text = original.chars().collect::>(); - new_text.extend(to_append.chars()); - new_text.into_iter().collect() - } - - pub fn insert_at_index(original: &str, to_splice: &str, index: usize) -> String { - let mut new_text = original.chars().collect::>(); - new_text.splice(index.., to_splice.chars()); - new_text.into_iter().collect() - } - - pub fn insert_at_range( - original: &str, - to_splice: &str, - start: usize, - end: usize, - ) -> String { - let mut new_text = original.chars().collect::>(); - new_text.splice(start..end, to_splice.chars()); - new_text.into_iter().collect() - } - - /// Get the live state of a set of attributes. - /// Although the function only currently tests one attribute, in the future it may be important to inspect many attributes, compare them, or do additional logic. - #[tracing::instrument(level = "trace", ret)] - pub fn get_live_state(attributes: &HashMap) -> OdiliaResult { - match attributes.get("live") { - None => Err(OdiliaError::NoAttributeError("live".to_string())), - Some(live) => Ok(serde_plain::from_str(live)?), - } - } - - /// if the aria-live attribute is set to "polite", then set the priority of the message to speak once all other messages are done - /// if the aria-live attribute is set to "assertive", then set the priority of the message to speak immediately, stop all other messages, and do not interrupt that piece of speech - /// otherwise, do not continue - pub fn live_to_priority(live_str: &AriaLive) -> Priority { - match live_str { - AriaLive::Assertive => Priority::Important, - AriaLive::Polite => Priority::Notification, - _ => Priority::Message, - } - } - - #[tracing::instrument(level = "trace", ret)] - pub fn get_atomic_state(attributes: &HashMap) -> OdiliaResult { - match attributes.get("atomic") { - None => Err(OdiliaError::NoAttributeError("atomic".to_string())), - Some(atomic) => Ok(serde_plain::from_str(atomic)?), - } - } - - pub fn get_string_within_bounds( - start_pos: usize, - update_length: usize, - ) -> impl Fn((usize, char)) -> Option { - move |(index, chr)| { - let is_after_start = index >= start_pos; - let is_before_end = index <= start_pos + update_length; - if is_after_start && is_before_end { - Some(chr) - } else { - None - } - } - } - - pub fn get_string_without_bounds( - start_pos: usize, - update_length: usize, - ) -> impl Fn((usize, char)) -> Option { - move |(index, chr)| { - let is_before_start = index < start_pos; - let is_after_end = index > start_pos + update_length; - if is_before_start || is_after_end { - Some(chr) - } else { - None - } - } - } - - #[tracing::instrument(level = "debug", skip(state), err)] - pub async fn dispatch( - state: &ScreenReaderState, - event: &TextChangedEvent, - ) -> eyre::Result<()> { - match event.operation { - Operation::Insert => insert_or_delete(state, event, true).await?, - Operation::Delete => insert_or_delete(state, event, false).await?, - }; - Ok(()) - } - - #[tracing::instrument(level = "debug", skip(state))] - pub async fn speak_insertion( - state: &ScreenReaderState, - event: &TextChangedEvent, - attributes: &HashMap, - cache_text: &str, - ) -> OdiliaResult<()> { - // note, you should update the text before this happens, since this could potentially end the function - let live = get_live_state(attributes)?; - let atomic = get_atomic_state(attributes)?; - // if the atomic state is true, then read out the entite piece of text - // if atomic state is false, then only read out the portion which has been added - // otherwise, do not continue through this function - let text_to_say = - if atomic { cache_text.to_string() } else { (&event.text).into() }; - let priority = live_to_priority(&live); - state.say(priority, text_to_say).await; - Ok(()) - } - - /// The `insert` boolean, if set to true, will update the text in the cache. - /// If it is set to false, the selection will be removed. - /// The [`TextChangedEvent::operation`] value will *NOT* be checked by this function. - #[tracing::instrument(level = "debug", skip(state), err)] - pub async fn insert_or_delete( - state: &ScreenReaderState, - event: &TextChangedEvent, - insert: bool, - ) -> eyre::Result<()> { - let accessible = state.new_accessible(event).await?; - let cache_item = state.get_or_create_event_object_to_cache(event).await?; - let updated_text: String = (&event.text).into(); - let current_text = cache_item.text; - let (start_pos, update_length) = - (usize::try_from(event.start_pos)?, usize::try_from(event.length)?); - // if this is an insert, figure out if we shuld announce anything, then speak it; - // only after should we try to update the cache - if insert { - let attributes = accessible.get_attributes().await?; - let _: OdiliaResult<()> = - speak_insertion(state, event, &attributes, ¤t_text).await; - } - - let text_selection_from_cache: String = current_text - .char_indices() - .filter_map(get_string_within_bounds(start_pos, update_length)) - .collect(); - let selection_matches_update = text_selection_from_cache == updated_text; - let insert_has_not_occured = insert && !selection_matches_update; - let remove_has_not_occured = !insert && selection_matches_update; - if insert_has_not_occured { - state.cache.modify_item( - &cache_item.object, - update_string_insert(start_pos, update_length, &updated_text), - )?; - } else if remove_has_not_occured { - state.cache.modify_item(&cache_item.object, move |cache_item| { - cache_item.text = cache_item - .text - .char_indices() - .filter_map(get_string_without_bounds( - start_pos, - update_length, - )) - .collect(); - })?; - } - Ok(()) - } -} - -mod children_changed { - use crate::state::ScreenReaderState; - use atspi_common::{events::object::ChildrenChangedEvent, Operation}; - use odilia_cache::CacheItem; - use odilia_common::{cache::AccessiblePrimitive, result::OdiliaResult}; - use std::sync::Arc; - - #[tracing::instrument(level = "debug", skip(state), err)] - pub async fn dispatch( - state: &ScreenReaderState, - event: &ChildrenChangedEvent, - ) -> eyre::Result<()> { - // Dispatch based on kind - match event.operation { - Operation::Insert => add(state, event).await?, - Operation::Delete => remove(state, event)?, - } - Ok(()) - } - #[tracing::instrument(level = "debug", skip(state), err)] - pub async fn add( - state: &ScreenReaderState, - event: &ChildrenChangedEvent, - ) -> eyre::Result<()> { - let accessible = get_child_primitive(event) - .into_accessible(state.atspi.connection()) - .await?; - let _: OdiliaResult = - state.cache.get_or_create(&accessible, Arc::clone(&state.cache)).await; - tracing::debug!("Add a single item to cache."); - Ok(()) - } - fn get_child_primitive(event: &ChildrenChangedEvent) -> AccessiblePrimitive { - event.child.clone().into() - } - #[tracing::instrument(level = "debug", skip(state), ret, err)] - pub fn remove(state: &ScreenReaderState, event: &ChildrenChangedEvent) -> eyre::Result<()> { - let prim = get_child_primitive(event); - state.cache.remove(&prim); - tracing::debug!("Remove a single item from cache."); - Ok(()) - } -} - -mod text_caret_moved { - use crate::state::ScreenReaderState; - use atspi_common::events::object::TextCaretMovedEvent; - use atspi_common::Granularity; - use odilia_cache::CacheItem; - use odilia_common::errors::{CacheError, OdiliaError}; - use ssip_client_async::Priority; - use std::{ - cmp::{max, min}, - sync::atomic::Ordering, - }; - use tracing::debug; - - #[tracing::instrument(level = "debug", ret, err)] - pub async fn new_position( - new_item: CacheItem, - old_item: CacheItem, - new_position: usize, - old_position: usize, - ) -> Result { - let new_id = new_item.object.clone(); - let old_id = old_item.object.clone(); - - // if the user has moved into a new item, then also read a whole line. - debug!("{new_id:?},{old_id:?}"); - debug!("{old_position},{new_position}"); - if new_id != old_id { - return Ok(new_item - .get_string_at_offset(new_position, Granularity::Line) - .await? - .0); - } - let first_position = min(new_position, old_position); - let last_position = max(new_position, old_position); - // if there is one character between the old and new position - if new_position.abs_diff(old_position) == 1 { - return Ok(new_item - .get_string_at_offset(first_position, Granularity::Char) - .await? - .0); - } - let first_word = new_item - .get_string_at_offset(first_position, Granularity::Word) - .await?; - let last_word = old_item - .get_string_at_offset(last_position, Granularity::Word) - .await?; - // if words are the same - if first_word == last_word || - // if the end position of the first word immediately preceeds the start of the second word - first_word.2.abs_diff(last_word.1) == 1 - { - return new_item.get_text(first_position, last_position); - } - // if the user has somehow from the beginning to the end. Usually happens with Home, the End. - if first_position == 0 && last_position == new_item.text.len() { - return Ok(new_item.text.clone()); - } - Ok(new_item - .get_string_at_offset(new_position, Granularity::Line) - .await? - .0) - } - - /// this must be checked *before* writing an accessible to the hsitory. - /// if this is checked after writing, it may give inaccurate results. - /// that said, this is a *guess* and not a guarentee. - /// TODO: make this a testable function, anything which queries "state" is not testable - async fn is_tab_navigation( - state: &ScreenReaderState, - event: &TextCaretMovedEvent, - ) -> eyre::Result { - let current_caret_pos = event.position; - // if the caret position is not at 0, we know that it is not a tab navigation, this is because tab will automatically set the cursor position at 0. - if current_caret_pos != 0 { - return Ok(false); - } - // Hopefully this shouldn't happen, but technically the caret may change before any other event happens. Since we already know that the caret position is 0, it may be a caret moved event - let last_accessible = match state.history_item(0) { - Some(acc) => state.get_or_create_cache_item(acc).await?, - None => return Ok(true), - }; - // likewise when getting the second-most recently focused accessible; we need the second-most recent accessible because it is possible that a tab navigation happened, which focused something before (or after) the caret moved events gets called, meaning the second-most recent accessible may be the only different accessible. - // if the accessible is focused before the event happens, the last_accessible variable will be the same as current_accessible. - // if the accessible is focused after the event happens, then the last_accessible will be different - let previous_caret_pos = state.previous_caret_position.load(Ordering::Relaxed); - let current_accessible = state.get_or_create_event_object_to_cache(event).await?; - // if we know that the previous caret position was not 0, and the current and previous accessibles are the same, we know that this is NOT a tab navigation. - if previous_caret_pos != 0 && current_accessible.object == last_accessible.object { - return Ok(false); - } - // otherwise, it probably was a tab navigation - Ok(true) - } - - // TODO: left/right vs. up/down, and use generated speech - #[tracing::instrument(level = "debug", skip(state), err)] - pub async fn text_cursor_moved( - state: &ScreenReaderState, - event: &TextCaretMovedEvent, - ) -> eyre::Result<()> { - if is_tab_navigation(state, event).await? { - return Ok(()); - } - let new_item = state.get_or_create_event_object_to_cache(event).await?; - - let new_prim = new_item.object.clone(); - let text = match state.history_item(0) { - Some(old_prim) => { - let old_pos = state.previous_caret_position.load(Ordering::Relaxed); - let old_item = - state.cache.get(&old_prim).ok_or(CacheError::NoItem)?; - let new_pos = event.position; - new_position( - new_item, - old_item, - new_pos.try_into() - .expect("Can not convert between i32 and usize"), - old_pos, - ) - .await? - } - None => { - // if no previous item exists, as in the screen reader has just loaded, then read out the whole item. - new_item.get_string_at_offset(0, Granularity::Paragraph).await?.0 - } - }; - state.say(Priority::Text, text).await; - state.update_accessible(new_prim); - Ok(()) - } - - #[tracing::instrument(level = "debug", skip(state), err)] - pub async fn dispatch( - state: &ScreenReaderState, - event: &TextCaretMovedEvent, - ) -> eyre::Result<()> { - text_cursor_moved(state, event).await?; - - state.previous_caret_position.store( - event.position - .try_into() - .expect("Converting from an i32 to a usize must not fail"), - Ordering::Relaxed, - ); - Ok(()) - } -} // end of text_caret_moved - -mod state_changed { - use crate::state::ScreenReaderState; - use atspi_common::{events::object::StateChangedEvent, State}; - use odilia_common::cache::AccessiblePrimitive; - - /// Update the state of an item in the cache using a `StateChanged` event and the `ScreenReaderState` as context. - /// This writes to the value in-place, and does not clone any values. - pub fn update_state( - state: &ScreenReaderState, - a11y: &AccessiblePrimitive, - state_changed: State, - active: bool, - ) -> eyre::Result { - if active { - Ok(state.cache.modify_item(a11y, |cache_item| { - cache_item.states.remove(state_changed); - })?) - } else { - Ok(state.cache.modify_item(a11y, |cache_item| { - cache_item.states.insert(state_changed); - })?) - } - } - - #[tracing::instrument(level = "debug", skip(state), err)] - pub async fn dispatch( - state: &ScreenReaderState, - event: &StateChangedEvent, - ) -> eyre::Result<()> { - let state_value = event.enabled; - // update cache with state of item - let a11y_prim = AccessiblePrimitive::from_event(event); - if update_state(state, &a11y_prim, event.state, state_value)? { - tracing::trace!("Updating of the state was not successful! The item with id {:?} was not found in the cache.", a11y_prim.id); - } else { - tracing::trace!("Updated the state of accessible with ID {:?}, and state {:?} to {state_value}.", a11y_prim.id, event.state); - } - // enabled can only be 1 or 0, but is not a boolean over dbus - match (event.state, event.enabled) { - (State::Focused, true) => focused(state, event).await?, - (state, enabled) => tracing::trace!( - "Ignoring state_changed event with unknown kind: {:?}/{}", - state, - enabled - ), - } - Ok(()) - } - - #[tracing::instrument(level = "debug", skip(state), err)] - pub async fn focused( - state: &ScreenReaderState, - event: &StateChangedEvent, - ) -> eyre::Result<()> { - let accessible = state.get_or_create_event_object_to_cache(event).await?; - if let Some(curr) = state.history_item(0) { - if curr == accessible.object { - return Ok(()); - } - } - - let (name, description, relation) = tokio::try_join!( - accessible.name(), - accessible.description(), - accessible.get_relation_set(), - )?; - state.update_accessible(accessible.object.clone()); - tracing::debug!( - "Focus event received on: {:?} with role {}", - accessible.object.id, - accessible.role, - ); - tracing::debug!("Relations: {:?}", relation); - - state.say( - ssip_client_async::Priority::Text, - format!("{name}, {0}. {description}", accessible.role), - ) - .await; - - state.update_accessible(accessible.object); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use crate::events::object::text_caret_moved::new_position; - use atspi_common::{Interface, InterfaceSet, Role, State, StateSet}; - use atspi_connection::AccessibilityConnection; - use lazy_static::lazy_static; - use odilia_cache::{Cache, CacheItem}; - use odilia_common::cache::AccessiblePrimitive; - use std::sync::Arc; - use tokio_test::block_on; - - static A11Y_PARAGRAPH_STRING: &str = "The AT-SPI (Assistive Technology Service Provider Interface) enables users of Linux to use their computer without sighted assistance. It was originally developed at Sun Microsystems, before they were purchased by Oracle."; - lazy_static! { - static ref ZBUS_CONN: AccessibilityConnection = - #[allow(clippy::unwrap_used)] - block_on(AccessibilityConnection::new()).unwrap(); - static ref CACHE_ARC: Arc = - Arc::new(Cache::new(ZBUS_CONN.connection().clone())); - static ref A11Y_PARAGRAPH_ITEM: CacheItem = CacheItem { - object: AccessiblePrimitive { - id: "/org/a11y/atspi/accessible/1".to_string(), - sender: ":1.2".into(), - }, - app: AccessiblePrimitive { - id: "/org/a11y/atspi/accessible/root".to_string(), - sender: ":1.2".into() - }, - parent: AccessiblePrimitive { - id: "/otg/a11y/atspi/accessible/1".to_string(), - sender: ":1.2".into(), - } - .into(), - index: Some(323), - children_num: Some(0), - interfaces: InterfaceSet::new( - Interface::Accessible - | Interface::Collection | Interface::Component - | Interface::Hyperlink | Interface::Hypertext - | Interface::Text - ), - role: Role::Paragraph, - states: StateSet::new( - State::Enabled | State::Opaque | State::Showing | State::Visible - ), - text: A11Y_PARAGRAPH_STRING.to_string(), - children: Vec::new(), - cache: Arc::downgrade(&CACHE_ARC), - }; - static ref ANSWER_VALUES: [(CacheItem, CacheItem, u32, u32, &'static str); 9] = [ - (A11Y_PARAGRAPH_ITEM.clone(), A11Y_PARAGRAPH_ITEM.clone(), 4, 3, " "), - (A11Y_PARAGRAPH_ITEM.clone(), A11Y_PARAGRAPH_ITEM.clone(), 3, 4, " "), - (A11Y_PARAGRAPH_ITEM.clone(), A11Y_PARAGRAPH_ITEM.clone(), 0, 3, "The"), - (A11Y_PARAGRAPH_ITEM.clone(), A11Y_PARAGRAPH_ITEM.clone(), 3, 0, "The"), - ( - A11Y_PARAGRAPH_ITEM.clone(), - A11Y_PARAGRAPH_ITEM.clone(), - 169, - 182, - "Microsystems," - ), - ( - A11Y_PARAGRAPH_ITEM.clone(), - A11Y_PARAGRAPH_ITEM.clone(), - 77, - 83, - " Linux" - ), - ( - A11Y_PARAGRAPH_ITEM.clone(), - A11Y_PARAGRAPH_ITEM.clone(), - 181, - 189, - ", before" - ), - ( - A11Y_PARAGRAPH_ITEM.clone(), - A11Y_PARAGRAPH_ITEM.clone(), - 0, - 220, - A11Y_PARAGRAPH_STRING, - ), - ( - A11Y_PARAGRAPH_ITEM.clone(), - A11Y_PARAGRAPH_ITEM.clone(), - 220, - 0, - A11Y_PARAGRAPH_STRING, - ), - ]; - } - - macro_rules! check_answer_values { - ($idx:literal) => { - assert_eq!( - tokio_test::block_on(new_position( - ANSWER_VALUES[$idx].0.clone(), - ANSWER_VALUES[$idx].1.clone(), - ANSWER_VALUES[$idx].2.try_into().unwrap(), - ANSWER_VALUES[$idx].3.try_into().unwrap(), - )) - .unwrap(), - ANSWER_VALUES[$idx].4.to_string(), - ); - }; - } - - #[test] - fn test_text_navigation_one_letter() { - check_answer_values!(0); - } - #[test] - fn test_text_navigation_one_letter_back() { - check_answer_values!(1); - } - #[test] - fn test_text_navigation_one_word_back() { - check_answer_values!(2); - } - #[test] - fn test_text_navigation_one_word() { - check_answer_values!(3); - } - #[test] - fn test_text_navigation_one_word_with_punctuation_after() { - check_answer_values!(4); - } - #[test] - fn test_text_navigation_one_word_with_space() { - check_answer_values!(5); - } - #[test] - fn test_text_navigation_one_word_with_punctutation_first() { - check_answer_values!(6); - } - #[test] - fn test_text_navigation_full_item_front_to_back() { - check_answer_values!(7); - } - #[test] - fn test_text_navigation_full_item_back_to_front() { - check_answer_values!(8); - } -} diff --git a/odilia/src/main.rs b/odilia/src/main.rs index 03ea383..6b5dd0d 100644 --- a/odilia/src/main.rs +++ b/odilia/src/main.rs @@ -25,8 +25,9 @@ use crate::state::LastCaretPos; use crate::state::LastFocused; use crate::state::ScreenReaderState; use crate::state::Speech; -use crate::tower::{CacheEvent, cache_event::ActiveAppEvent}; use crate::tower::Handlers; +use crate::tower::{cache_event::ActiveAppEvent, CacheEvent}; +use atspi::RelationType; use clap::Parser; use eyre::WrapErr; use figment::{ @@ -39,7 +40,7 @@ use odilia_common::{ errors::OdiliaError, settings::ApplicationConfig, }; -use odilia_input::sr_event_receiver; + use odilia_notify::listen_to_dbus_notifications; use ssip::Priority; use ssip::Request as SSIPRequest; @@ -118,9 +119,48 @@ use crate::tower::state_changed::{Focused, Unfocused}; #[tracing::instrument(ret)] async fn focused(state_changed: CacheEvent) -> impl TryIntoCommands { + //because the current command implementation doesn't allow for multiple speak commands without interrupting the previous utterance, this is more or less an accumulating buffer for that utterance + let mut utterance_buffer = String::new(); + //does this have a text or a name? + // in order for the borrow checker to not scream that we move ownership of item.text, therefore making item partially moved, we only take a reference here, because in truth the only thing that we need to know is if the string is empty, because the extending of the buffer will imply a clone anyway + let text = &state_changed.item.text; + if text.is_empty() { + //then the label can either be the accessible name, the description, or the relations set, aka labeled by another object + //unfortunately, the or_else function of result doesn't accept async cloasures or cloasures with async blocks, so we can't use lazy loading here at the moment. The performance penalty is minimal however, because this should be in cache anyway + let mut label = state_changed + .item + .name() + .await + .or(state_changed.item.description().await)?; + // if both of those errored out, we'd know, because we'd be out of here + //otherwise, if this is empty too, we try to use the relations set to find the element labeling this one + if label.is_empty() { + label = state_changed + .item + .get_relation_set() + .await? + .into_iter() + // we only need entries which contain the wanted relationship, only labeled by for now + .filter(|elem| elem.0 == RelationType::LabelledBy) + // we have to remove the first item of the entries, because it's constant now that we filtered by it + //furthermore, after doing this, we'd have something like Vec>, which is not what we need, we need something like Vec, so we do both the flattening of the structure and the mapping in one function call + .flat_map(|this| this.1) + // from a collection of items, to a collection of strings, in this case the text of those labels, because yes, technically there can be more than one + .map(|this| this.text) + // gather all that into a string, separated by newlines or spaces I think + .collect(); + } + utterance_buffer += &label; + } else { + //then just append to the buffer and be done with it + utterance_buffer += text; + } + let role = state_changed.item.role; + //there has to be a space between the accessible name of an object and its role, so insert it now + utterance_buffer += &format!(" {}", role.name().to_owned()); Ok(vec![ Focus(state_changed.item.object).into(), - Speak(state_changed.item.text, Priority::Text).into(), + Speak(utterance_buffer, Priority::Text).into(), ]) } @@ -207,10 +247,9 @@ async fn main() -> eyre::Result<()> { { tracing::error!("Could not set AT-SPI2 IsEnabled property because: {}", e); } - let (sr_event_tx, sr_event_rx) = mpsc::channel(128); // this is the channel which handles all SSIP commands. If SSIP is not allowed to operate on a separate task, then waiting for the receiving message can block other long-running operations like structural navigation. // Although in the future, this may possibly be resolved through a proper cache, I think it still makes sense to separate SSIP's IO operations to a separate task. - // Like the channel above, it is very important that this is *never* full, since it can cause deadlocking if the other task sending the request is working with zbus. + // it is very important that this is *never* full, since it can cause deadlocking if the other task sending the request is working with zbus. let (ssip_req_tx, ssip_req_rx) = mpsc::channel::(128); let (mut ev_tx, ev_rx) = futures::channel::mpsc::channel::>(10_000); @@ -249,26 +288,13 @@ async fn main() -> eyre::Result<()> { let ssip_event_receiver = odilia_tts::handle_ssip_commands(ssip, ssip_req_rx, token.clone()) .map(|r| r.wrap_err("Could no process SSIP request")); - /* - let atspi_event_receiver = - events::receive(Arc::clone(&state), atspi_event_tx, token.clone()) - .map(|()| Ok::<_, eyre::Report>(())); - let atspi_event_processor = - events::process(Arc::clone(&state), atspi_event_rx, token.clone()) - .map(|()| Ok::<_, eyre::Report>(())); - */ - let odilia_event_receiver = sr_event_receiver(sr_event_tx, token.clone()) - .map(|r| r.wrap_err("Could not process Odilia events")); - let odilia_event_processor = - events::sr_event(Arc::clone(&state), sr_event_rx, token.clone()) - .map(|r| r.wrap_err("Could not process Odilia event")); let notification_task = notifications_monitor(Arc::clone(&state), token.clone()) .map(|r| r.wrap_err("Could not process signal shutdown.")); let mut stream = state.atspi.event_stream(); // There is a reason we are not reading from the event stream directly. // This `MessageStream` can only store 64 events in its buffer. // And, even if it could store more (it can via options), `zbus` specifically states that: - // > You must ensure a MessageStream is continuously polled or you will experience hangs. + // You must ensure a MessageStream is continuously polled or you will experience hangs. // So, we continually poll it here, then receive it on the other end. // Additioanlly, since sending is not async, but simply errors when there is an issue, this will // help us avoid hangs. @@ -282,10 +308,6 @@ async fn main() -> eyre::Result<()> { }; let atspi_handlers_task = handlers.atspi_handler(ev_rx); - //tracker.spawn(atspi_event_receiver); - //tracker.spawn(atspi_event_processor); - tracker.spawn(odilia_event_receiver); - tracker.spawn(odilia_event_processor); tracker.spawn(ssip_event_receiver); tracker.spawn(notification_task); tracker.spawn(atspi_handlers_task); diff --git a/odilia/src/state.rs b/odilia/src/state.rs index f41a854..102455d 100644 --- a/odilia/src/state.rs +++ b/odilia/src/state.rs @@ -24,7 +24,6 @@ use odilia_common::{ cache::AccessiblePrimitive, command::CommandType, errors::{CacheError, OdiliaError}, - modes::ScreenReaderMode, settings::{speech::PunctuationSpellingMode, ApplicationConfig}, types::TextSelectionArea, Result as OdiliaResult, @@ -37,7 +36,6 @@ pub(crate) struct ScreenReaderState { pub dbus: DBusProxy<'static>, pub ssip: Sender, pub previous_caret_position: Arc, - pub mode: Mutex, pub accessible_history: Arc>>, pub event_history: Mutex>, pub cache: Arc, @@ -133,13 +131,6 @@ where } } -enum ConfigType { - CliOverride, - XDGConfigHome, - Etc, - CreateDefault, -} - impl ScreenReaderState { #[tracing::instrument(skip_all)] pub async fn new( @@ -157,8 +148,6 @@ impl ScreenReaderState { .await .wrap_err("Failed to create org.freedesktop.DBus proxy")?; - let mode = Mutex::new(ScreenReaderMode { name: "CommandMode".to_string() }); - tracing::debug!("Reading configuration"); let previous_caret_position = Arc::new(AtomicUsize::new(0)); @@ -212,7 +201,6 @@ impl ScreenReaderState { dbus, ssip, previous_caret_position, - mode, accessible_history, event_history, cache, diff --git a/odilia/src/tower/cache_event.rs b/odilia/src/tower/cache_event.rs index ab99670..0158f09 100644 --- a/odilia/src/tower/cache_event.rs +++ b/odilia/src/tower/cache_event.rs @@ -8,16 +8,16 @@ use std::fmt::Debug; use std::{future::Future, marker::PhantomData, sync::Arc}; use zbus::{names::UniqueName, zvariant::ObjectPath}; -pub type CacheEvent = CacheEventPredicate; -pub type ActiveAppEvent = CacheEventPredicate; +pub type CacheEvent = EventPredicate; +pub type ActiveAppEvent = EventPredicate; #[derive(Debug, Clone, Deref, DerefMut)] -pub struct CacheEventInner { +pub struct InnerEvent { #[target] pub inner: E, pub item: CacheItem, } -impl CacheEventInner +impl InnerEvent where E: EventProperties + Debug, { @@ -27,24 +27,18 @@ where } #[derive(Debug, Clone, Deref, DerefMut)] -pub struct CacheEventPredicate< - E: EventProperties + Debug, - P: Predicate<(E, Arc)>, -> { +pub struct EventPredicate)>> { #[target] pub inner: E, pub item: CacheItem, _marker: PhantomData

, } -impl CacheEventPredicate +impl EventPredicate where E: EventProperties + Debug + Clone, P: Predicate<(E, Arc)>, { - pub fn from_cache_event( - ce: CacheEventInner, - state: Arc, - ) -> Option { + pub fn from_cache_event(ce: InnerEvent, state: Arc) -> Option { if P::test(&(ce.inner.clone(), state)) { return Some(Self { inner: ce.inner, item: ce.item, _marker: PhantomData }); } @@ -74,7 +68,7 @@ where } } -impl TryFromState, E> for CacheEventInner +impl TryFromState, E> for InnerEvent where E: EventProperties + Debug + Clone, { @@ -87,12 +81,12 @@ where let proxy = a11y.into_accessible(state.connection()).await?; let cache_item = state.cache.get_or_create(&proxy, Arc::clone(&state.cache)).await?; - Ok(CacheEventInner::new(event, cache_item)) + Ok(InnerEvent::new(event, cache_item)) } } } -impl TryFromState, E> for CacheEventPredicate +impl TryFromState, E> for EventPredicate where E: EventProperties + Debug + Clone, P: Predicate<(E, Arc)> + Debug, @@ -106,8 +100,8 @@ where let proxy = a11y.into_accessible(state.connection()).await?; let cache_item = state.cache.get_or_create(&proxy, Arc::clone(&state.cache)).await?; - let cache_event = CacheEventInner::new(event.clone(), cache_item); - CacheEventPredicate::from_cache_event(cache_event, state).ok_or( + let cache_event = InnerEvent::new(event.clone(), cache_item); + EventPredicate::from_cache_event(cache_event, state).ok_or( OdiliaError::PredicateFailure(format!( "Predicate cache event {} failed for event {:?}", std::any::type_name::

(), @@ -118,7 +112,7 @@ where } } -impl EventProperties for CacheEventPredicate +impl EventProperties for EventPredicate where E: EventProperties + Debug, P: Predicate<(E, Arc)>, diff --git a/odilia/src/tower/choice.rs b/odilia/src/tower/choice.rs index 96f0e57..28a538a 100644 --- a/odilia/src/tower/choice.rs +++ b/odilia/src/tower/choice.rs @@ -52,12 +52,6 @@ where pub fn new() -> Self { ChoiceService { services: BTreeMap::new(), _marker: PhantomData } } - pub fn insert(&mut self, k: K, s: S) - where - K: Ord, - { - self.services.insert(k, s); - } pub fn entry(&mut self, k: K) -> Entry where K: Ord, diff --git a/odilia/src/tower/service_set.rs b/odilia/src/tower/service_set.rs index 29bb764..1cd0539 100644 --- a/odilia/src/tower/service_set.rs +++ b/odilia/src/tower/service_set.rs @@ -19,9 +19,6 @@ impl Default for ServiceSet { } } impl ServiceSet { - pub fn new>(services: I) -> Self { - ServiceSet { services: services.into_iter().collect() } - } pub fn push(&mut self, svc: S) { self.services.push(svc); } diff --git a/odilia/src/tower/state_changed.rs b/odilia/src/tower/state_changed.rs index f832d88..3d11ec5 100644 --- a/odilia/src/tower/state_changed.rs +++ b/odilia/src/tower/state_changed.rs @@ -115,6 +115,7 @@ macro_rules! impl_refinement_type { pub struct AnyState; impl Predicate for AnyState { + #[allow(clippy::too_many_lines)] fn test(outer: &AtspiState) -> bool { match *outer { AtspiState::Invalid => >::test(outer),