fix(a11y): listen to screen reader changes from org.a11y.Bus

Enables the screen reader toggle to toggle when using the shortcut.
This commit is contained in:
Michael Aaron Murphy 2026-01-09 21:32:07 +01:00 committed by Michael Murphy
parent ded50f418e
commit c05dad00de
17 changed files with 163 additions and 135 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-a11y-manager-subscription"
version = "1.0.0-beta6"
version = "1.0.2"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-accessibility-subscription"
version = "1.0.0-beta6"
version = "1.0.2"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true
@ -10,6 +10,6 @@ publish = true
cosmic-dbus-a11y = { git = "https://github.com/pop-os/dbus-settings-bindings" }
futures = "0.3.31"
iced_futures = { git = "https://github.com/pop-os/libcosmic" }
tokio = "1.48.0"
tokio = { version = "1.48.0", features = ["sync", "time"] }
tracing = "0.1.41"
zbus = "5.12.0"
zbus = "5"

View file

@ -2,132 +2,154 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_dbus_a11y::*;
use futures::FutureExt;
use futures::{self, SinkExt, StreamExt, select};
use futures::{self, SinkExt, StreamExt};
use iced_futures::{Subscription, stream};
use std::fmt::Debug;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use zbus::Connection;
#[derive(Debug, Clone)]
pub enum DBusUpdate {
pub enum Response {
Error(String),
Status(bool),
Init(bool, UnboundedSender<DBusRequest>),
IsEnabled(bool),
ScreenReader(bool),
Init(bool, UnboundedSender<Request>),
}
pub enum DBusRequest {
Status(bool),
pub enum Request {
/// Enable the org.a11y.Bus
Enable(bool),
/// Enable the screen reader feature of org.a11y.Bus
ScreenReader(bool),
}
#[derive(Debug)]
pub enum State {
Ready,
Waiting(Connection, u8, bool, UnboundedReceiver<DBusRequest>),
Finished,
pub struct State {
conn: Connection,
retry: u8,
enabled: bool,
rx: UnboundedReceiver<Request>,
}
pub fn subscription() -> Subscription<DBusUpdate> {
pub fn subscription() -> Subscription<Response> {
struct MyId;
Subscription::run_with_id(
std::any::TypeId::of::<MyId>(),
stream::channel(50, move |mut output| async move {
let mut state = State::Ready;
loop {
state = start_listening(state, &mut output).await;
stream::channel(1, move |mut output| async move {
if let Some(state) = State::new(&mut output).await {
state.listen(&mut output).await;
}
futures::future::pending::<()>().await;
}),
)
}
async fn start_listening(
state: State,
output: &mut futures::channel::mpsc::Sender<DBusUpdate>,
) -> State {
match state {
State::Ready => {
let conn = match Connection::session().await.map_err(|e| e.to_string()) {
Ok(conn) => conn,
Err(e) => {
_ = output.send(DBusUpdate::Error(e)).await;
return State::Finished;
}
};
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut enabled = false;
if let Ok(proxy) = StatusProxy::new(&conn).await {
if let Ok(status) = proxy.screen_reader_enabled().await {
enabled = status;
}
impl State {
pub async fn new(output: &mut futures::channel::mpsc::Sender<Response>) -> Option<Self> {
let conn = match Connection::session().await.map_err(|e| e.to_string()) {
Ok(conn) => conn,
Err(e) => {
_ = output.send(Response::Error(e)).await;
return None;
}
_ = output.send(DBusUpdate::Init(enabled, tx)).await;
State::Waiting(conn, 20, enabled, rx)
}
State::Waiting(conn, mut retry, mut enabled, mut rx) => {
let Ok(proxy) = StatusProxy::new(&conn).await else {
if retry == 0 {
tracing::error!("Accessibility Status is unavailable.");
return State::Finished;
} else {
_ = tokio::time::sleep(tokio::time::Duration::from_secs(
2_u64.pow(retry as u32),
))
.await;
retry -= 1;
return State::Waiting(conn, retry, enabled, rx);
}
};
retry = 20;
let mut watch_changes = proxy.receive_screen_reader_enabled_changed().await;
};
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut enabled = false;
if let Ok(proxy) = StatusProxy::new(&conn).await {
if let Ok(status) = proxy.screen_reader_enabled().await {
if enabled != status {
_ = output.send(DBusUpdate::Status(enabled));
}
enabled = status;
}
}
_ = output.send(Response::Init(enabled, tx)).await;
Some(State {
conn,
retry: 20,
enabled,
rx,
})
}
loop {
if let Ok(status) = proxy.screen_reader_enabled().await {
if enabled != status {
_ = output.send(DBusUpdate::Status(enabled));
}
enabled = status;
pub async fn listen(mut self, output: &mut futures::channel::mpsc::Sender<Response>) {
loop {
let Ok(proxy) = StatusProxy::new(&self.conn).await else {
if self.retry == 0 {
tracing::error!("Accessibility Status is unavailable.");
return;
} else {
_ = tokio::time::sleep(tokio::time::Duration::from_secs(
2_u64.pow(self.retry as u32),
))
.await;
self.retry -= 1;
continue;
}
};
let mut next_change = Box::pin(watch_changes.next()).fuse();
let mut next_request = Box::pin(rx.recv()).fuse();
self.retry = 20;
select! {
v = next_request => {
match v {
Some(DBusRequest::Status(is_enabled)) => {
// Set status
enabled = is_enabled;
_ = proxy.set_is_enabled(is_enabled).await;
_ = proxy.set_screen_reader_enabled(is_enabled).await;
}
None => return State::Finished,
}
}
v = next_change => {
match v {
Some(f) => {
if let Ok(enabled) = f.get().await {
_ = output.send(DBusUpdate::Status(enabled));
}
}
None => break,
};
}
let Ok(properties_proxy) =
zbus::fdo::PropertiesProxy::new(&self.conn, "org.a11y.Bus", "/org/a11y/bus").await
else {
tracing::error!("org.a11y.Bus properties proxy failed");
return;
};
let Ok(mut properties_changed_stream) =
properties_proxy.receive_properties_changed().await
else {
tracing::error!("org.a11y.Bus receive properties changed failed");
return;
};
if let Ok(status) = proxy.screen_reader_enabled().await {
if self.enabled != status {
_ = output.send(Response::ScreenReader(self.enabled)).await;
}
self.enabled = status;
}
State::Waiting(conn, retry, enabled, rx)
let requests_fut = Box::pin(async {
while let Some(request) = self.rx.recv().await {
match request {
Request::ScreenReader(is_enabled) => {
_ = proxy.set_is_enabled(is_enabled).await;
_ = proxy.set_screen_reader_enabled(is_enabled).await;
}
Request::Enable(is_enabled) => {
_ = proxy.set_is_enabled(is_enabled).await;
}
}
}
});
let properties_fut = Box::pin(async {
while let Some(signal) = properties_changed_stream.next().await {
if let Ok(args) = signal.args() {
for (name, value) in args.changed_properties().iter() {
match *name {
"IsEnabled" => {
if let Ok(status) = value.downcast_ref::<bool>() {
_ = output.send(Response::IsEnabled(status)).await;
}
}
"ScreenReaderEnabled" => {
if let Ok(status) = value.downcast_ref::<bool>() {
_ = output.send(Response::ScreenReader(status)).await;
}
}
_ => (),
}
}
}
}
});
futures::future::select(properties_fut, requests_fut).await;
}
State::Finished => futures::future::pending().await,
}
}

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-airplane-mode-subscription"
version = "1.0.0-beta6"
version = "1.0.2"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-bluetooth-subscription"
version = "1.0.0-beta6"
version = "1.0.2"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-network-manager-subscription"
version = "1.0.0-beta6"
version = "1.0.2"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true

View file

@ -496,7 +496,7 @@ impl SettingsSecretAgent {
setting_attributes.insert("uuid", &conn_uuid);
setting_attributes.insert("setting_name", &setting_name);
let mut search_items = collection
let search_items = collection
.search_items(setting_attributes.clone())
.await
.map_err(|e| Arc::new(e))?;

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-daemon-subscription"
version = "1.0.0-beta6"
version = "1.0.2"
edition = "2024"
rust-version.workspace = true
publish = true

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-sound-subscription"
version = "1.0.0-beta6"
version = "1.0.2"
edition = "2024"
rust-version.workspace = true
license = "MPL-2.0"

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings-upower-subscription"
version = "1.0.0-beta6"
version = "1.0.2"
edition = "2024"
license = "MPL-2.0"
rust-version.workspace = true