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

24
Cargo.lock generated
View file

@ -1606,7 +1606,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-pipewire" name = "cosmic-pipewire"
version = "1.0.0-beta6" version = "1.0.0"
dependencies = [ dependencies = [
"intmap", "intmap",
"libspa", "libspa",
@ -1657,7 +1657,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings" name = "cosmic-settings"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"accounts-zbus", "accounts-zbus",
"anyhow", "anyhow",
@ -1739,7 +1739,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-a11y-manager-subscription" name = "cosmic-settings-a11y-manager-subscription"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"cosmic-protocols", "cosmic-protocols",
"iced_futures", "iced_futures",
@ -1752,7 +1752,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-accessibility-subscription" name = "cosmic-settings-accessibility-subscription"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"cosmic-dbus-a11y", "cosmic-dbus-a11y",
"futures", "futures",
@ -1764,7 +1764,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-airplane-mode-subscription" name = "cosmic-settings-airplane-mode-subscription"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"futures", "futures",
"iced_futures", "iced_futures",
@ -1775,7 +1775,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-bluetooth-subscription" name = "cosmic-settings-bluetooth-subscription"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"bluez-zbus", "bluez-zbus",
"futures", "futures",
@ -1817,7 +1817,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-daemon-subscription" name = "cosmic-settings-daemon-subscription"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"futures", "futures",
"iced_futures", "iced_futures",
@ -1829,7 +1829,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-network-manager-subscription" name = "cosmic-settings-network-manager-subscription"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"cosmic-dbus-networkmanager", "cosmic-dbus-networkmanager",
@ -1847,7 +1847,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-page" name = "cosmic-settings-page"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"derive_setters", "derive_setters",
"downcast-rs 2.0.2", "downcast-rs 2.0.2",
@ -1860,7 +1860,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-sound-subscription" name = "cosmic-settings-sound-subscription"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"cosmic-pipewire", "cosmic-pipewire",
"futures", "futures",
@ -1874,7 +1874,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-upower-subscription" name = "cosmic-settings-upower-subscription"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"futures", "futures",
"iced_futures", "iced_futures",
@ -1887,7 +1887,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-wallpaper" name = "cosmic-settings-wallpaper"
version = "1.0.0-beta6" version = "1.0.2"
dependencies = [ dependencies = [
"cosmic-bg-config", "cosmic-bg-config",
"cosmic-randr-shell", "cosmic-randr-shell",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "cosmic-settings" name = "cosmic-settings"
version = "1.0.0-beta6" version = "1.0.2"
edition = "2024" edition = "2024"
license = "GPL-3.0-only" license = "GPL-3.0-only"
publish = false publish = false

View file

@ -9,9 +9,7 @@ use cosmic::{
pub use cosmic_comp_config::ZoomMovement; pub use cosmic_comp_config::ZoomMovement;
use cosmic_config::CosmicConfigEntry; use cosmic_config::CosmicConfigEntry;
use cosmic_settings_a11y_manager_subscription as cosmic_a11y_manager; use cosmic_settings_a11y_manager_subscription as cosmic_a11y_manager;
use cosmic_settings_accessibility_subscription::{ use cosmic_settings_accessibility_subscription as a11y_bus;
DBusRequest, DBusUpdate, subscription as a11y_subscription,
};
use cosmic_settings_daemon_config::CosmicSettingsDaemonConfig; use cosmic_settings_daemon_config::CosmicSettingsDaemonConfig;
use cosmic_settings_page::{ use cosmic_settings_page::{
self as page, Insert, self as page, Insert,
@ -39,7 +37,7 @@ pub struct Page {
high_contrast: Option<bool>, high_contrast: Option<bool>,
daemon_config: CosmicSettingsDaemonConfig, daemon_config: CosmicSettingsDaemonConfig,
daemon_helper: cosmic_config::Config, daemon_helper: cosmic_config::Config,
dbus_sender: Option<UnboundedSender<DBusRequest>>, dbus_sender: Option<UnboundedSender<a11y_bus::Request>>,
reader_enabled: bool, reader_enabled: bool,
} }
@ -76,18 +74,18 @@ impl Default for Page {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
A11yBus(a11y_bus::Response),
Event(cosmic_a11y_manager::AccessibilityEvent), Event(cosmic_a11y_manager::AccessibilityEvent),
HighContrast(bool),
ProtocolUnavailable, ProtocolUnavailable,
Return, Return,
HighContrast(bool), ScreenReaderEnabled(bool),
SystemTheme(Box<cosmic::cosmic_theme::Theme>),
SetScreenInverted(bool),
SetScreenFilterActive(bool), SetScreenFilterActive(bool),
SetScreenFilterSelection(ColorFilter), SetScreenFilterSelection(ColorFilter),
Surface(surface::Action), SetScreenInverted(bool),
SetSoundMono(bool), SetSoundMono(bool),
DBusUpdate(DBusUpdate), Surface(surface::Action),
ScreenReaderEnabled(bool), SystemTheme(Box<cosmic::cosmic_theme::Theme>),
} }
impl From<Message> for crate::pages::Message { impl From<Message> for crate::pages::Message {
@ -169,7 +167,7 @@ impl page::Page<crate::pages::Message> for Page {
&self, &self,
_core: &cosmic::Core, _core: &cosmic::Core,
) -> cosmic::iced::Subscription<crate::pages::Message> { ) -> cosmic::iced::Subscription<crate::pages::Message> {
a11y_subscription().map(|m| super::Message::Accessibility(Message::DBusUpdate(m))) a11y_bus::subscription().map(|m| super::Message::Accessibility(Message::A11yBus(m)))
} }
} }
@ -434,16 +432,17 @@ impl Page {
tracing::error!("{err:?}"); tracing::error!("{err:?}");
} }
} }
Message::DBusUpdate(update) => match update { Message::A11yBus(update) => match update {
DBusUpdate::Error(err) => { a11y_bus::Response::Error(err) => {
tracing::error!("{err}"); tracing::error!("{err}");
let _ = self.dbus_sender.take(); let _ = self.dbus_sender.take();
self.reader_enabled = false; self.reader_enabled = false;
} }
DBusUpdate::Status(enabled) => { a11y_bus::Response::ScreenReader(enabled) => {
self.reader_enabled = enabled; self.reader_enabled = enabled;
} }
DBusUpdate::Init(enabled, tx) => { a11y_bus::Response::IsEnabled(_) => (),
a11y_bus::Response::Init(enabled, tx) => {
self.reader_enabled = enabled; self.reader_enabled = enabled;
self.dbus_sender = Some(tx); self.dbus_sender = Some(tx);
} }
@ -451,7 +450,7 @@ impl Page {
Message::ScreenReaderEnabled(enabled) => { Message::ScreenReaderEnabled(enabled) => {
if let Some(tx) = &self.dbus_sender { if let Some(tx) = &self.dbus_sender {
self.reader_enabled = enabled; self.reader_enabled = enabled;
let _ = tx.send(DBusRequest::Status(enabled)); let _ = tx.send(a11y_bus::Request::ScreenReader(enabled));
} else { } else {
self.reader_enabled = false; self.reader_enabled = false;
} }

View file

@ -1,7 +1,8 @@
[package] [package]
name = "cosmic-pipewire" name = "cosmic-pipewire"
version = "1.0.0-beta6" version = "1.0.0"
edition = "2024" edition = "2024"
repository = "https://github.com/pop-os/cosmic-settings"
rust-version.workspace = true rust-version.workspace = true
license = "MPL-2.0" license = "MPL-2.0"
publish = true publish = true

6
debian/changelog vendored
View file

@ -1,3 +1,9 @@
cosmic-settings (1.0.2) noble; urgency=medium
* Released version
-- Michael Murphy <michael@mmurphy.dev> Fri, 09 Jan 2026 21:31:17 +0100
cosmic-settings (0.1.0) jammy; urgency=medium cosmic-settings (0.1.0) jammy; urgency=medium
* Project in development * Project in development

View file

@ -1,6 +1,6 @@
[package] [package]
name = "cosmic-settings-page" name = "cosmic-settings-page"
version = "1.0.0-beta6" version = "1.0.2"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View file

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

View file

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

View file

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

View file

@ -2,132 +2,154 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use cosmic_dbus_a11y::*; use cosmic_dbus_a11y::*;
use futures::FutureExt; use futures::{self, SinkExt, StreamExt};
use futures::{self, SinkExt, StreamExt, select};
use iced_futures::{Subscription, stream}; use iced_futures::{Subscription, stream};
use std::fmt::Debug; use std::fmt::Debug;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use zbus::Connection; use zbus::Connection;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum DBusUpdate { pub enum Response {
Error(String), Error(String),
Status(bool), IsEnabled(bool),
Init(bool, UnboundedSender<DBusRequest>), ScreenReader(bool),
Init(bool, UnboundedSender<Request>),
} }
pub enum DBusRequest { pub enum Request {
Status(bool), /// Enable the org.a11y.Bus
Enable(bool),
/// Enable the screen reader feature of org.a11y.Bus
ScreenReader(bool),
} }
#[derive(Debug)] #[derive(Debug)]
pub enum State { pub struct State {
Ready, conn: Connection,
Waiting(Connection, u8, bool, UnboundedReceiver<DBusRequest>), retry: u8,
Finished, enabled: bool,
rx: UnboundedReceiver<Request>,
} }
pub fn subscription() -> Subscription<DBusUpdate> { pub fn subscription() -> Subscription<Response> {
struct MyId; struct MyId;
Subscription::run_with_id( Subscription::run_with_id(
std::any::TypeId::of::<MyId>(), std::any::TypeId::of::<MyId>(),
stream::channel(50, move |mut output| async move { stream::channel(1, move |mut output| async move {
let mut state = State::Ready; if let Some(state) = State::new(&mut output).await {
state.listen(&mut output).await;
loop {
state = start_listening(state, &mut output).await;
} }
futures::future::pending::<()>().await;
}), }),
) )
} }
async fn start_listening( impl State {
state: State, pub async fn new(output: &mut futures::channel::mpsc::Sender<Response>) -> Option<Self> {
output: &mut futures::channel::mpsc::Sender<DBusUpdate>, let conn = match Connection::session().await.map_err(|e| e.to_string()) {
) -> State { Ok(conn) => conn,
match state { Err(e) => {
State::Ready => { _ = output.send(Response::Error(e)).await;
let conn = match Connection::session().await.map_err(|e| e.to_string()) { return None;
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;
}
} }
_ = output.send(DBusUpdate::Init(enabled, tx)).await; };
State::Waiting(conn, 20, enabled, rx) let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
} let mut enabled = false;
State::Waiting(conn, mut retry, mut enabled, mut rx) => { if let Ok(proxy) = StatusProxy::new(&conn).await {
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;
if let Ok(status) = proxy.screen_reader_enabled().await { if let Ok(status) = proxy.screen_reader_enabled().await {
if enabled != status {
_ = output.send(DBusUpdate::Status(enabled));
}
enabled = status; enabled = status;
} }
}
_ = output.send(Response::Init(enabled, tx)).await;
Some(State {
conn,
retry: 20,
enabled,
rx,
})
}
loop { pub async fn listen(mut self, output: &mut futures::channel::mpsc::Sender<Response>) {
if let Ok(status) = proxy.screen_reader_enabled().await { loop {
if enabled != status { let Ok(proxy) = StatusProxy::new(&self.conn).await else {
_ = output.send(DBusUpdate::Status(enabled)); if self.retry == 0 {
} tracing::error!("Accessibility Status is unavailable.");
enabled = status; 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(); self.retry = 20;
let mut next_request = Box::pin(rx.recv()).fuse();
select! { let Ok(properties_proxy) =
v = next_request => { zbus::fdo::PropertiesProxy::new(&self.conn, "org.a11y.Bus", "/org/a11y/bus").await
match v { else {
Some(DBusRequest::Status(is_enabled)) => { tracing::error!("org.a11y.Bus properties proxy failed");
// Set status return;
enabled = is_enabled; };
_ = proxy.set_is_enabled(is_enabled).await;
_ = proxy.set_screen_reader_enabled(is_enabled).await; let Ok(mut properties_changed_stream) =
} properties_proxy.receive_properties_changed().await
None => return State::Finished, else {
} tracing::error!("org.a11y.Bus receive properties changed failed");
} return;
v = next_change => { };
match v {
Some(f) => { if let Ok(status) = proxy.screen_reader_enabled().await {
if let Ok(enabled) = f.get().await { if self.enabled != status {
_ = output.send(DBusUpdate::Status(enabled)); _ = output.send(Response::ScreenReader(self.enabled)).await;
}
}
None => break,
};
}
} }
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] [package]
name = "cosmic-settings-airplane-mode-subscription" name = "cosmic-settings-airplane-mode-subscription"
version = "1.0.0-beta6" version = "1.0.2"
edition = "2024" edition = "2024"
license = "MPL-2.0" license = "MPL-2.0"
rust-version.workspace = true rust-version.workspace = true

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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