Fix review comments, add range testing

This commit is contained in:
Vicki Pfau 2025-05-23 18:59:00 -07:00 committed by Jeremy Whiting
parent 8028c34f79
commit 7d838ae6f6
2 changed files with 187 additions and 205 deletions

View file

@ -633,11 +633,8 @@ impl ScreenReader1 {
#[interface(name = "com.steampowered.SteamOSManager1.ScreenReader1")] #[interface(name = "com.steampowered.SteamOSManager1.ScreenReader1")]
impl ScreenReader1 { impl ScreenReader1 {
#[zbus(property)] #[zbus(property)]
async fn enabled(&self) -> fdo::Result<bool> { async fn enabled(&self) -> bool {
match self.screen_reader.enabled().await { self.screen_reader.enabled()
Ok(enabled) => Ok(enabled),
Err(e) => Err(to_zbus_fdo_error(e)),
}
} }
#[zbus(property)] #[zbus(property)]
@ -649,11 +646,8 @@ impl ScreenReader1 {
} }
#[zbus(property)] #[zbus(property)]
async fn rate(&self) -> fdo::Result<f64> { async fn rate(&self) -> f64 {
match self.screen_reader.rate().await { self.screen_reader.rate()
Ok(rate) => Ok(rate),
Err(e) => Err(to_zbus_fdo_error(e)),
}
} }
#[zbus(property)] #[zbus(property)]
@ -665,11 +659,8 @@ impl ScreenReader1 {
} }
#[zbus(property)] #[zbus(property)]
async fn pitch(&self) -> fdo::Result<f64> { async fn pitch(&self) -> f64 {
match self.screen_reader.pitch().await { self.screen_reader.pitch()
Ok(pitch) => Ok(pitch),
Err(e) => Err(to_zbus_fdo_error(e)),
}
} }
#[zbus(property)] #[zbus(property)]
@ -681,11 +672,8 @@ impl ScreenReader1 {
} }
#[zbus(property)] #[zbus(property)]
async fn volume(&self) -> fdo::Result<f64> { async fn volume(&self) -> f64 {
match self.screen_reader.volume().await { self.screen_reader.volume()
Ok(volume) => Ok(volume),
Err(e) => Err(to_zbus_fdo_error(e)),
}
} }
#[zbus(property)] #[zbus(property)]

View file

@ -5,22 +5,41 @@
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
use anyhow::{anyhow, Result}; use anyhow::{anyhow, bail, ensure, Result};
use std::fs::File; use lazy_static::lazy_static;
use std::io::Read; use serde_json::{Map, Value};
use std::io::Write; use std::collections::HashMap;
//use std::process::{Child, Command}; use std::ops::RangeInclusive;
use serde_json::{json, Value}; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use tracing::{info, warn}; use tokio::fs::{read_to_string, write};
use tracing::{debug, error, info, trace, warn};
#[cfg(not(test))]
use xdg::BaseDirectories;
use zbus::Connection; use zbus::Connection;
#[cfg(test)]
use crate::path;
use crate::systemd::SystemdUnit; use crate::systemd::SystemdUnit;
const ORCA_SETTINGS: &str = "/home/deck/.local/share/orca/user-settings.conf"; #[cfg(not(test))]
const ORCA_SETTINGS: &str = "orca/user-settings.conf";
const PITCH_SETTING: &str = "average-pitch"; const PITCH_SETTING: &str = "average-pitch";
const RATE_SETTING: &str = "rate"; const RATE_SETTING: &str = "rate";
const VOLUME_SETTING: &str = "gain"; const VOLUME_SETTING: &str = "gain";
const ENABLE_SETTING: &str = "enableSpeech";
const PITCH_DEFAULT: f64 = 1.0;
const RATE_DEFAULT: f64 = 1.0;
const VOLUME_DEFAULT: f64 = 1.0;
lazy_static! {
static ref VALID_SETTINGS: HashMap<&'static str, RangeInclusive<f64>> = HashMap::from_iter([
(PITCH_SETTING, (0.0..=10.0)),
(RATE_SETTING, (0.0..=100.0)),
(VOLUME_SETTING, (0.0..=10.0)),
]);
}
pub(crate) struct OrcaManager<'dbus> { pub(crate) struct OrcaManager<'dbus> {
orca_unit: SystemdUnit<'dbus>, orca_unit: SystemdUnit<'dbus>,
@ -34,256 +53,231 @@ impl<'dbus> OrcaManager<'dbus> {
pub async fn new(connection: &Connection) -> Result<OrcaManager<'dbus>> { pub async fn new(connection: &Connection) -> Result<OrcaManager<'dbus>> {
let mut manager = OrcaManager { let mut manager = OrcaManager {
orca_unit: SystemdUnit::new(connection.clone(), "orca.service").await?, orca_unit: SystemdUnit::new(connection.clone(), "orca.service").await?,
rate: 1.0, rate: RATE_DEFAULT,
pitch: 1.0, pitch: PITCH_DEFAULT,
volume: 1.0, volume: VOLUME_DEFAULT,
enabled: true, enabled: true,
}; };
manager.load_values()?; let _ = manager
.load_values()
.await
.inspect_err(|e| warn!("Failed to load orca configuration: {e}"));
Ok(manager) Ok(manager)
} }
pub async fn enabled(&self) -> Result<bool> { #[cfg(not(test))]
// Check if screen reader is enabled fn settings_path(&self) -> Result<PathBuf> {
Ok(self.enabled) let xdg_base = BaseDirectories::new();
Ok(xdg_base
.get_data_home()
.ok_or(anyhow!("No XDG_DATA_HOME found"))?
.join(ORCA_SETTINGS))
} }
pub async fn set_enabled(&mut self, enable: bool) -> std::io::Result<()> { #[cfg(test)]
// Set screen reader enabled based on value of enable fn settings_path(&self) -> Result<PathBuf> {
Ok(path("orca-settings.conf"))
}
pub fn enabled(&self) -> bool {
self.enabled
}
pub async fn set_enabled(&mut self, enable: bool) -> Result<()> {
if self.enabled == enable {
return Ok(());
}
if enable { if enable {
// Enable screen reader gsettings // Enable screen reader gsettings
let _ = Command::new("gsettings") Command::new("gsettings")
.args([ .args([
"set", "set",
"org.gnome.desktop.a11y.applications", "org.gnome.desktop.a11y.applications",
"screen-reader-enabled", "screen-reader-enabled",
"true", "true",
]) ])
.spawn(); .spawn()?;
// Set orca enabled also // Set orca enabled also
let _setting_result = self.set_orca_enabled(true); self.set_orca_enabled(true).await?;
let _result = self.restart_orca().await; self.restart_orca().await?;
} else { } else {
// Disable screen reader gsettings // Disable screen reader gsettings
let _ = Command::new("gsettings") Command::new("gsettings")
.args([ .args([
"set", "set",
"org.gnome.desktop.a11y.applications", "org.gnome.desktop.a11y.applications",
"screen-reader-enabled", "screen-reader-enabled",
"false", "false",
]) ])
.spawn(); .spawn()?;
// Set orca disabled also // Set orca disabled also
let _setting_result = self.set_orca_enabled(false); self.set_orca_enabled(false).await?;
// Stop orca self.stop_orca().await?;
let _result = self.stop_orca().await;
} }
self.enabled = enable; self.enabled = enable;
Ok(()) Ok(())
} }
pub async fn pitch(&self) -> Result<f64> { pub fn pitch(&self) -> f64 {
Ok(self.pitch) self.pitch
} }
pub async fn set_pitch(&mut self, pitch: f64) -> Result<()> { pub async fn set_pitch(&mut self, pitch: f64) -> Result<()> {
info!("set_pitch called with {:?}", pitch); trace!("set_pitch called with {pitch}");
let result = self.set_orca_option(PITCH_SETTING.to_owned(), pitch); self.set_orca_option(PITCH_SETTING, pitch).await?;
match result { self.pitch = pitch;
Ok(_) => { Ok(())
self.pitch = pitch;
Ok(())
}
Err(_) => Err(anyhow!("Unable to set orca pitch value")),
}
} }
pub async fn rate(&self) -> Result<f64> { pub fn rate(&self) -> f64 {
Ok(self.rate) self.rate
} }
pub async fn set_rate(&mut self, rate: f64) -> Result<()> { pub async fn set_rate(&mut self, rate: f64) -> Result<()> {
info!("set_rate called with {:?}", rate); trace!("set_rate called with {rate}");
let result = self.set_orca_option(RATE_SETTING.to_owned(), rate); self.set_orca_option(RATE_SETTING, rate).await?;
match result { self.rate = rate;
Ok(_) => { Ok(())
self.rate = rate;
Ok(())
}
Err(_) => Err(anyhow!("Unable to set orca rate")),
}
} }
pub async fn volume(&self) -> Result<f64> { pub fn volume(&self) -> f64 {
Ok(self.volume) self.volume
} }
pub async fn set_volume(&mut self, volume: f64) -> Result<()> { pub async fn set_volume(&mut self, volume: f64) -> Result<()> {
info!("set_volume called with {:?}", volume); trace!("set_volume called with {volume}");
let result = self.set_orca_option(VOLUME_SETTING.to_owned(), volume); self.set_orca_option(VOLUME_SETTING, volume).await?;
match result { self.volume = volume;
Ok(_) => { Ok(())
self.volume = volume;
Ok(())
}
Err(_) => Err(anyhow!("Unable to set orca volume")),
}
} }
fn set_orca_enabled(&mut self, enabled: bool) -> Result<()> { async fn set_orca_enabled(&mut self, enabled: bool) -> Result<()> {
// Change json file // Change json file
let mut file = File::open(ORCA_SETTINGS)?; let data = read_to_string(self.settings_path()?).await?;
let mut data = String::new();
file.read_to_string(&mut data)?;
let mut json: Value = serde_json::from_str(&data)?; let mut json: Value = serde_json::from_str(&data)?;
if let Some(general) = json.get_mut("general") { let general = json
if let Some(enable_speech) = general.get_mut("enableSpeech") { .as_object_mut()
*enable_speech = json!(&enabled); .ok_or(anyhow!("orca user-settings.conf json is not an object"))?
} else { .entry("general")
warn!("No enabledSpeech value in general in orca settings"); .or_insert(Value::Object(Map::new()));
} general
} else { .as_object_mut()
warn!("No general section in orca settings"); .ok_or(anyhow!("orca user-settings.conf general is not an object"))?
} .insert(ENABLE_SETTING.to_string(), Value::Bool(enabled));
data = serde_json::to_string_pretty(&json)?; let data = serde_json::to_string_pretty(&json)?;
Ok(write(self.settings_path()?, data.as_bytes()).await?)
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<()> { async fn load_values(&mut self) -> Result<()> {
info!("Loading orca values from user-settings.conf"); debug!("Loading orca values from user-settings.conf");
let mut file = File::open(ORCA_SETTINGS)?; let data = read_to_string(self.settings_path()?).await?;
let mut data = String::new();
file.read_to_string(&mut data)?;
let json: Value = serde_json::from_str(&data)?; let json: Value = serde_json::from_str(&data)?;
if let Some(profiles) = json.get("profiles") { let Some(default_voice) = json
if let Some(default_profile) = profiles.get("default") { .get("profiles")
if let Some(voices) = default_profile.get("voices") { .and_then(|profiles| profiles.get("default"))
if let Some(default_voice) = voices.get("default") { .and_then(|default_profile| default_profile.get("voices"))
if let Some(pitch) = default_voice.get(PITCH_SETTING.to_owned()) { .and_then(|voices| voices.get("default"))
self.pitch = pitch else {
.as_f64() warn!("Orca user-settings.conf missing default voice");
.expect("Unable to convert orca pitch setting to float value"); self.pitch = PITCH_DEFAULT;
} else { self.rate = RATE_DEFAULT;
warn!("Unable to load default pitch from orca user-settings.conf"); self.volume = VOLUME_DEFAULT;
} return Ok(());
};
if let Some(rate) = default_voice.get(RATE_SETTING.to_owned()) { if let Some(pitch) = default_voice.get(PITCH_SETTING) {
self.rate = rate self.pitch = pitch.as_f64().unwrap_or_else(|| {
.as_f64() error!("Unable to convert orca pitch setting to float value");
.expect("Unable to convert orca rate setting to float value"); PITCH_DEFAULT
} 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 { } else {
warn!("Orca user-settings.conf missing profiles"); warn!("Unable to load default pitch from orca user-settings.conf");
self.pitch = PITCH_DEFAULT;
}
if let Some(rate) = default_voice.get(RATE_SETTING) {
self.rate = rate.as_f64().unwrap_or_else(|| {
error!("Unable to convert orca rate setting to float value");
RATE_DEFAULT
});
} else {
warn!("Unable to load default voice rate from orca user-settings.conf");
}
if let Some(volume) = default_voice.get(VOLUME_SETTING) {
self.volume = volume.as_f64().unwrap_or_else(|| {
error!("Unable to convert orca volume value to float value");
VOLUME_DEFAULT
});
} else {
warn!("Unable to load default voice volume from orca user-settings.conf");
} }
info!( info!(
"Done loading orca user-settings.conf, values: Rate: {:?}, Pitch: {:?}, Volume: {:?}", "Done loading orca user-settings.conf, values: Rate: {}, Pitch: {}, Volume: {}",
self.rate, self.pitch, self.volume self.rate, self.pitch, self.volume
); );
Ok(()) Ok(())
} }
fn set_orca_option(&self, option: String, value: f64) -> Result<()> { async fn set_orca_option(&self, option: &str, value: f64) -> Result<()> {
// Verify option is one we know about if let Some(range) = VALID_SETTINGS.get(option) {
// Verify value is in range ensure!(
// Change json file range.contains(&value),
let mut file = File::open(ORCA_SETTINGS)?; "orca option {option} value {value} out of range"
let mut data = String::new(); );
file.read_to_string(&mut data)?; } else {
bail!("Invalid orca option {option}");
}
let data = read_to_string(self.settings_path()?).await?;
let mut json: Value = serde_json::from_str(&data)?; let mut json: Value = serde_json::from_str(&data)?;
if let Some(profiles) = json.get_mut("profiles") { let profiles = json
if let Some(default_profile) = profiles.get_mut("default") { .as_object_mut()
if let Some(voices) = default_profile.get_mut("voices") { .ok_or(anyhow!("orca user-settings.conf json is not an object"))?
if let Some(default_voice) = voices.get_mut("default") { .entry("profiles")
if let Some(mut_option) = default_voice.get_mut(&option) { .or_insert(Value::Object(Map::new()));
*mut_option = json!(value); let default_profile = profiles
} else { .as_object_mut()
let object = default_voice.as_object_mut().ok_or(anyhow!( .ok_or(anyhow!("orca user-settings.conf profiles is not an object"))?
"Unable to generate mutable object for adding {:?} with value {:?}", .entry("default")
&option, .or_insert(Value::Object(Map::new()));
value let voices = default_profile
))?; .as_object_mut()
object.insert(option, json!(value)); .ok_or(anyhow!(
} "orca user-settings.conf default profile is not an object"
} else { ))?
warn!( .entry("voices")
"No default voice in voices list to set {:?} to {:?} in", .or_insert(Value::Object(Map::new()));
&option, value let default_voice = voices
); .as_object_mut()
} .ok_or(anyhow!("orca user-settings.conf voices is not an object"))?
} else { .entry("default")
warn!( .or_insert(Value::Object(Map::new()));
"No voices in default profile to set {:?} to {:?} in", default_voice
&option, value .as_object_mut()
); .ok_or(anyhow!(
} "orca user-settings.conf default voice is not an object"
} else { ))?
warn!("No default profile to set {:?} to {:?} in", &option, value); .insert(option.to_string(), value.into());
}
} else {
warn!("No profiles in orca user-settings.conf to modify");
}
data = serde_json::to_string_pretty(&json)?; let data = serde_json::to_string_pretty(&json)?;
Ok(write(self.settings_path()?, data.as_bytes()).await?)
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<()> { async fn restart_orca(&self) -> Result<()> {
info!("Restarting orca..."); trace!("Restarting orca...");
let _result = self.orca_unit.restart().await; self.orca_unit.restart().await
info!("Done restarting orca...");
Ok(())
} }
async fn stop_orca(&self) -> Result<()> { async fn stop_orca(&self) -> Result<()> {
// Stop orca user unit trace!("Stopping orca...");
info!("Stopping orca..."); self.orca_unit.stop().await
let _result = self.orca_unit.stop().await;
info!("Done stopping orca...");
Ok(())
} }
} }