mirror of
https://gitlab.steamos.cloud/holo/steamos-manager.git
synced 2025-07-05 06:00:30 -04:00
Add screenreader support to steamos-manager.
Add ScreenReader1 interface to xml to enable/disable using screen reader. Implements getting and setting pitch, rate, volume, enabled. Restarts orca when any of the above properties are changed. Load values from orca user-settings.conf Use systemd unit to start/stop/restart orca.
This commit is contained in:
parent
b926dbd50b
commit
4fd9ccdd2e
6 changed files with 432 additions and 0 deletions
25
Cargo.lock
generated
25
Cargo.lock
generated
|
@ -437,6 +437,12 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
|
@ -725,6 +731,12 @@ version = "1.0.20"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
|
@ -745,6 +757,18 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.20"
|
||||
|
@ -815,6 +839,7 @@ dependencies = [
|
|||
"num_enum",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
|
|
|
@ -19,6 +19,7 @@ nix = { version = "0.30", default-features = false, features = ["fs", "poll", "s
|
|||
num_enum = "0.7"
|
||||
regex = "1"
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
tempfile = "3"
|
||||
tokio = { version = "1", default-features = false, features = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync"] }
|
||||
|
|
|
@ -298,6 +298,40 @@
|
|||
|
||||
</interface>
|
||||
|
||||
<!--
|
||||
com.steampowered.SteamOSManager1.ScreenReader1
|
||||
@short_description: Optional interface for managing a screen reader.
|
||||
-->
|
||||
<interface name="com.steampowered.SteamOSManager1.ScreenReader1">
|
||||
<!--
|
||||
Enabled
|
||||
|
||||
True if screen reader is enabled, false otherwise.
|
||||
-->
|
||||
<property name="Enabled" type="b" access="readwrite"/>
|
||||
|
||||
<!--
|
||||
Rate
|
||||
|
||||
The rate of speech output. Valid values are 0 for slowest to 100 for fastest.
|
||||
-->
|
||||
<property name="Rate" type="d" access="readwrite"/>
|
||||
|
||||
<!--
|
||||
Pitch
|
||||
|
||||
The pitch for speech output. Valid values are 0.0 for lowest, and 10.0 for highest.
|
||||
-->
|
||||
<property name="Pitch" type="d" access="readwrite"/>
|
||||
|
||||
<!--
|
||||
Volume
|
||||
|
||||
The volume for speech output. Valid values ar 0.0 for off, 10.0 for highest.
|
||||
-->
|
||||
<property name="Volume" type="d" access="readwrite"/>
|
||||
</interface>
|
||||
|
||||
<!--
|
||||
com.steampowered.SteamOSManager1.Storage1
|
||||
@short_description: Optional interface for managing storage devices
|
||||
|
|
|
@ -25,6 +25,7 @@ mod job;
|
|||
mod manager;
|
||||
mod platform;
|
||||
mod process;
|
||||
mod screenreader;
|
||||
mod sls;
|
||||
mod systemd;
|
||||
mod udev;
|
||||
|
|
|
@ -31,6 +31,7 @@ use crate::power::{
|
|||
get_gpu_clocks, get_gpu_clocks_range, get_gpu_performance_level, get_gpu_power_profile,
|
||||
get_max_charge_level, get_platform_profile, tdp_limit_manager, TdpManagerCommand,
|
||||
};
|
||||
use crate::screenreader::OrcaManager;
|
||||
use crate::wifi::{
|
||||
get_wifi_backend, get_wifi_power_management_state, list_wifi_interfaces, WifiBackend,
|
||||
};
|
||||
|
@ -153,6 +154,10 @@ struct PerformanceProfile1 {
|
|||
tdp_limit_manager: UnboundedSender<TdpManagerCommand>,
|
||||
}
|
||||
|
||||
struct ScreenReader1 {
|
||||
screen_reader: OrcaManager<'static>,
|
||||
}
|
||||
|
||||
struct Storage1 {
|
||||
proxy: Proxy<'static>,
|
||||
job_manager: UnboundedSender<JobManagerCommand>,
|
||||
|
@ -618,6 +623,80 @@ impl PerformanceProfile1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl ScreenReader1 {
|
||||
async fn new(connection: &Connection) -> Result<ScreenReader1> {
|
||||
let screen_reader = OrcaManager::new(connection).await?;
|
||||
Ok(ScreenReader1 { screen_reader })
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(name = "com.steampowered.SteamOSManager1.ScreenReader1")]
|
||||
impl ScreenReader1 {
|
||||
#[zbus(property)]
|
||||
async fn enabled(&self) -> fdo::Result<bool> {
|
||||
match self.screen_reader.enabled().await {
|
||||
Ok(enabled) => Ok(enabled),
|
||||
Err(e) => Err(to_zbus_fdo_error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
async fn set_enabled(&mut self, enabled: bool) -> fdo::Result<()> {
|
||||
self.screen_reader
|
||||
.set_enabled(enabled)
|
||||
.await
|
||||
.map_err(to_zbus_fdo_error)
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
async fn rate(&self) -> fdo::Result<f64> {
|
||||
match self.screen_reader.rate().await {
|
||||
Ok(rate) => Ok(rate),
|
||||
Err(e) => Err(to_zbus_fdo_error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
async fn set_rate(&mut self, rate: f64) -> fdo::Result<()> {
|
||||
self.screen_reader
|
||||
.set_rate(rate)
|
||||
.await
|
||||
.map_err(to_zbus_fdo_error)
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
async fn pitch(&self) -> fdo::Result<f64> {
|
||||
match self.screen_reader.pitch().await {
|
||||
Ok(pitch) => Ok(pitch),
|
||||
Err(e) => Err(to_zbus_fdo_error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
async fn set_pitch(&mut self, pitch: f64) -> fdo::Result<()> {
|
||||
self.screen_reader
|
||||
.set_pitch(pitch)
|
||||
.await
|
||||
.map_err(to_zbus_fdo_error)
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
async fn volume(&self) -> fdo::Result<f64> {
|
||||
match self.screen_reader.volume().await {
|
||||
Ok(volume) => Ok(volume),
|
||||
Err(e) => Err(to_zbus_fdo_error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
async fn set_volume(&mut self, volume: f64) -> fdo::Result<()> {
|
||||
self.screen_reader
|
||||
.set_volume(volume)
|
||||
.await
|
||||
.map_err(to_zbus_fdo_error)
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(name = "com.steampowered.SteamOSManager1.Storage1")]
|
||||
impl Storage1 {
|
||||
async fn format_device(
|
||||
|
@ -919,6 +998,7 @@ pub(crate) async fn create_interfaces(
|
|||
proxy: proxy.clone(),
|
||||
channel: daemon,
|
||||
};
|
||||
let screen_reader = ScreenReader1::new(&session).await?;
|
||||
let wifi_debug = WifiDebug1 {
|
||||
proxy: proxy.clone(),
|
||||
};
|
||||
|
@ -971,6 +1051,8 @@ pub(crate) async fn create_interfaces(
|
|||
|
||||
object_server.at(MANAGER_PATH, manager2).await?;
|
||||
|
||||
object_server.at(MANAGER_PATH, screen_reader).await?;
|
||||
|
||||
if steam_deck_variant().await.unwrap_or_default() == SteamDeckVariant::Galileo {
|
||||
object_server.at(MANAGER_PATH, wifi_debug).await?;
|
||||
}
|
||||
|
|
289
src/screenreader.rs
Normal file
289
src/screenreader.rs
Normal file
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* Copyright © 2025 Collabora Ltd.
|
||||
* Copyright © 2025 Valve Software
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
//use std::process::{Child, Command};
|
||||
use serde_json::{json, Value};
|
||||
use std::process::Command;
|
||||
use tracing::{info, warn};
|
||||
use zbus::Connection;
|
||||
|
||||
use crate::systemd::SystemdUnit;
|
||||
|
||||
const ORCA_SETTINGS: &str = "/home/deck/.local/share/orca/user-settings.conf";
|
||||
const PITCH_SETTING: &str = "average-pitch";
|
||||
const RATE_SETTING: &str = "rate";
|
||||
const VOLUME_SETTING: &str = "gain";
|
||||
|
||||
pub(crate) struct OrcaManager<'dbus> {
|
||||
orca_unit: SystemdUnit<'dbus>,
|
||||
rate: f64,
|
||||
pitch: f64,
|
||||
volume: f64,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl<'dbus> OrcaManager<'dbus> {
|
||||
pub async fn new(connection: &Connection) -> Result<OrcaManager<'dbus>> {
|
||||
let mut manager = OrcaManager {
|
||||
orca_unit: SystemdUnit::new(connection.clone(), "orca.service").await?,
|
||||
rate: 1.0,
|
||||
pitch: 1.0,
|
||||
volume: 1.0,
|
||||
enabled: true,
|
||||
};
|
||||
manager.load_values()?;
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
pub async fn enabled(&self) -> Result<bool> {
|
||||
// Check if screen reader is enabled
|
||||
Ok(self.enabled)
|
||||
}
|
||||
|
||||
pub async fn set_enabled(&mut self, enable: bool) -> std::io::Result<()> {
|
||||
// Set screen reader enabled based on value of enable
|
||||
if enable {
|
||||
// Enable screen reader gsettings
|
||||
let _ = Command::new("gsettings")
|
||||
.args([
|
||||
"set",
|
||||
"org.gnome.desktop.a11y.applications",
|
||||
"screen-reader-enabled",
|
||||
"true",
|
||||
])
|
||||
.spawn();
|
||||
// Set orca enabled also
|
||||
let _setting_result = self.set_orca_enabled(true);
|
||||
let _result = self.restart_orca().await;
|
||||
} else {
|
||||
// Disable screen reader gsettings
|
||||
let _ = Command::new("gsettings")
|
||||
.args([
|
||||
"set",
|
||||
"org.gnome.desktop.a11y.applications",
|
||||
"screen-reader-enabled",
|
||||
"false",
|
||||
])
|
||||
.spawn();
|
||||
// Set orca disabled also
|
||||
let _setting_result = self.set_orca_enabled(false);
|
||||
// Stop orca
|
||||
let _result = self.stop_orca().await;
|
||||
}
|
||||
self.enabled = enable;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn pitch(&self) -> Result<f64> {
|
||||
Ok(self.pitch)
|
||||
}
|
||||
|
||||
pub async fn set_pitch(&mut self, pitch: f64) -> Result<()> {
|
||||
info!("set_pitch called with {:?}", pitch);
|
||||
|
||||
let result = self.set_orca_option(PITCH_SETTING.to_owned(), pitch);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
self.pitch = pitch;
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(anyhow!("Unable to set orca pitch value")),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rate(&self) -> Result<f64> {
|
||||
Ok(self.rate)
|
||||
}
|
||||
|
||||
pub async fn set_rate(&mut self, rate: f64) -> Result<()> {
|
||||
info!("set_rate called with {:?}", rate);
|
||||
|
||||
let result = self.set_orca_option(RATE_SETTING.to_owned(), rate);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
self.rate = rate;
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(anyhow!("Unable to set orca rate")),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn volume(&self) -> Result<f64> {
|
||||
Ok(self.volume)
|
||||
}
|
||||
|
||||
pub async fn set_volume(&mut self, volume: f64) -> Result<()> {
|
||||
info!("set_volume called with {:?}", volume);
|
||||
|
||||
let result = self.set_orca_option(VOLUME_SETTING.to_owned(), volume);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
self.volume = volume;
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(anyhow!("Unable to set orca volume")),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_orca_enabled(&mut self, enabled: bool) -> Result<()> {
|
||||
// Change json file
|
||||
let mut file = File::open(ORCA_SETTINGS)?;
|
||||
let mut data = String::new();
|
||||
file.read_to_string(&mut data)?;
|
||||
|
||||
let mut json: Value = serde_json::from_str(&data)?;
|
||||
|
||||
if let Some(general) = json.get_mut("general") {
|
||||
if let Some(enable_speech) = general.get_mut("enableSpeech") {
|
||||
*enable_speech = json!(&enabled);
|
||||
} else {
|
||||
warn!("No enabledSpeech value in general in orca settings");
|
||||
}
|
||||
} else {
|
||||
warn!("No general section in orca settings");
|
||||
}
|
||||
|
||||
data = serde_json::to_string_pretty(&json)?;
|
||||
|
||||
let mut out_file = File::create(ORCA_SETTINGS)?;
|
||||
match out_file.write_all(&data.into_bytes()) {
|
||||
Ok(_) => {
|
||||
self.enabled = enabled;
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(anyhow!("Unable to write orca settings file")),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_values(&mut self) -> Result<()> {
|
||||
info!("Loading orca values from user-settings.conf");
|
||||
let mut file = File::open(ORCA_SETTINGS)?;
|
||||
let mut data = String::new();
|
||||
file.read_to_string(&mut data)?;
|
||||
|
||||
let json: Value = serde_json::from_str(&data)?;
|
||||
|
||||
if let Some(profiles) = json.get("profiles") {
|
||||
if let Some(default_profile) = profiles.get("default") {
|
||||
if let Some(voices) = default_profile.get("voices") {
|
||||
if let Some(default_voice) = voices.get("default") {
|
||||
if let Some(pitch) = default_voice.get(PITCH_SETTING.to_owned()) {
|
||||
self.pitch = pitch
|
||||
.as_f64()
|
||||
.expect("Unable to convert orca pitch setting to float value");
|
||||
} else {
|
||||
warn!("Unable to load default pitch from orca user-settings.conf");
|
||||
}
|
||||
|
||||
if let Some(rate) = default_voice.get(RATE_SETTING.to_owned()) {
|
||||
self.rate = rate
|
||||
.as_f64()
|
||||
.expect("Unable to convert orca rate setting to float value");
|
||||
} else {
|
||||
warn!("Unable to load default voice rate from orca user-settings.conf");
|
||||
}
|
||||
|
||||
if let Some(volume) = default_voice.get(VOLUME_SETTING.to_owned()) {
|
||||
self.volume = volume
|
||||
.as_f64()
|
||||
.expect("Unable to convert orca volume value to float value");
|
||||
} else {
|
||||
warn!(
|
||||
"Unable to load default voice volume from orca user-settings.conf"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!("Orca user-settings.conf missing default voice");
|
||||
}
|
||||
} else {
|
||||
warn!("Orca user-settings.conf missing voices list");
|
||||
}
|
||||
} else {
|
||||
warn!("Orca user-settings.conf missing default profile");
|
||||
}
|
||||
} else {
|
||||
warn!("Orca user-settings.conf missing profiles");
|
||||
}
|
||||
info!(
|
||||
"Done loading orca user-settings.conf, values: Rate: {:?}, Pitch: {:?}, Volume: {:?}",
|
||||
self.rate, self.pitch, self.volume
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_orca_option(&self, option: String, value: f64) -> Result<()> {
|
||||
// Verify option is one we know about
|
||||
// Verify value is in range
|
||||
// Change json file
|
||||
let mut file = File::open(ORCA_SETTINGS)?;
|
||||
let mut data = String::new();
|
||||
file.read_to_string(&mut data)?;
|
||||
|
||||
let mut json: Value = serde_json::from_str(&data)?;
|
||||
|
||||
if let Some(profiles) = json.get_mut("profiles") {
|
||||
if let Some(default_profile) = profiles.get_mut("default") {
|
||||
if let Some(voices) = default_profile.get_mut("voices") {
|
||||
if let Some(default_voice) = voices.get_mut("default") {
|
||||
if let Some(mut_option) = default_voice.get_mut(&option) {
|
||||
*mut_option = json!(value);
|
||||
} else {
|
||||
let object = default_voice.as_object_mut().ok_or(anyhow!(
|
||||
"Unable to generate mutable object for adding {:?} with value {:?}",
|
||||
&option,
|
||||
value
|
||||
))?;
|
||||
object.insert(option, json!(value));
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"No default voice in voices list to set {:?} to {:?} in",
|
||||
&option, value
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"No voices in default profile to set {:?} to {:?} in",
|
||||
&option, value
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!("No default profile to set {:?} to {:?} in", &option, value);
|
||||
}
|
||||
} else {
|
||||
warn!("No profiles in orca user-settings.conf to modify");
|
||||
}
|
||||
|
||||
data = serde_json::to_string_pretty(&json)?;
|
||||
|
||||
let mut out_file = File::create(ORCA_SETTINGS)?;
|
||||
match out_file.write_all(&data.into_bytes()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(anyhow!("Unable to write orca settings file")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn restart_orca(&self) -> Result<()> {
|
||||
info!("Restarting orca...");
|
||||
let _result = self.orca_unit.restart().await;
|
||||
info!("Done restarting orca...");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_orca(&self) -> Result<()> {
|
||||
// Stop orca user unit
|
||||
info!("Stopping orca...");
|
||||
let _result = self.orca_unit.stop().await;
|
||||
info!("Done stopping orca...");
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue