mirror of
https://gitlab.steamos.cloud/holo/steamos-manager.git
synced 2025-07-18 12:16:43 -04:00
Merge branch 'work/whiting/screenreader' into 'master'
Add screenreader support to steamos-manager. See merge request holo/steamos-manager!1
This commit is contained in:
commit
334fc09a0b
14 changed files with 1015 additions and 5 deletions
|
@ -8,7 +8,7 @@ test:
|
||||||
tags:
|
tags:
|
||||||
- x86_64-linux-kvm-docker
|
- x86_64-linux-kvm-docker
|
||||||
script:
|
script:
|
||||||
- pacman -Sy --noconfirm --needed dbus rust
|
- pacman -Sy --noconfirm --needed dbus rust gsettings-desktop-schemas
|
||||||
- dbus-run-session cargo test
|
- dbus-run-session cargo test
|
||||||
|
|
||||||
workflow:
|
workflow:
|
||||||
|
|
218
Cargo.lock
generated
218
Cargo.lock
generated
|
@ -105,6 +105,16 @@ version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-expr"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e34e221e91c7eb5e8315b5c9cf1a61670938c0626451f954a51693ed44b37f45"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
"target-lexicon",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -295,12 +305,32 @@ version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
@ -320,12 +350,43 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-macro",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
@ -355,6 +416,91 @@ version = "0.31.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gio"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2a5c3829f5794cb15120db87707b2ec03720edff7ad09eb7b711b532e3fe747"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
|
"gio-sys",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gio-sys"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c501c495842c2b23cdacead803a5a343ca2a5d7a7ddaff14cc5f6cf22cfb92c2"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
"gio-sys",
|
||||||
|
"glib-macros",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"memchr",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib-macros"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebe6dc9ce29887c4b3b74d78d5ba473db160a258ae7ed883d23632ac7fed7bc9"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib-sys"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gobject-sys"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
|
@ -437,6 +583,12 @@ dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
@ -596,6 +748,12 @@ version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
@ -725,6 +883,12 @@ version = "1.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.219"
|
version = "1.0.219"
|
||||||
|
@ -745,6 +909,18 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
|
@ -783,6 +959,21 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.9"
|
version = "0.5.9"
|
||||||
|
@ -807,6 +998,7 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"clap",
|
"clap",
|
||||||
"config",
|
"config",
|
||||||
|
"gio",
|
||||||
"inotify",
|
"inotify",
|
||||||
"itertools",
|
"itertools",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
@ -815,6 +1007,7 @@ dependencies = [
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"strum",
|
"strum",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -862,6 +1055,25 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-deps"
|
||||||
|
version = "7.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-expr",
|
||||||
|
"heck",
|
||||||
|
"pkg-config",
|
||||||
|
"toml",
|
||||||
|
"version-compare",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "target-lexicon"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.20.0"
|
version = "3.20.0"
|
||||||
|
@ -1069,6 +1281,12 @@ version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version-compare"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
|
|
@ -11,6 +11,7 @@ anyhow = "1"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
clap = { version = "4.5", default-features = false, features = ["derive", "help", "std", "usage"] }
|
clap = { version = "4.5", default-features = false, features = ["derive", "help", "std", "usage"] }
|
||||||
config = { version = "0.15", default-features = false, features = ["async", "ini", "toml"] }
|
config = { version = "0.15", default-features = false, features = ["async", "ini", "toml"] }
|
||||||
|
gio = "0.20"
|
||||||
inotify = { version = "0.11", default-features = false, features = ["stream"] }
|
inotify = { version = "0.11", default-features = false, features = ["stream"] }
|
||||||
itertools = "0.14"
|
itertools = "0.14"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
|
@ -19,6 +20,7 @@ nix = { version = "0.30", default-features = false, features = ["fs", "poll", "s
|
||||||
num_enum = "0.7"
|
num_enum = "0.7"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
strum = { version = "0.27", features = ["derive"] }
|
strum = { version = "0.27", features = ["derive"] }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
tokio = { version = "1", default-features = false, features = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync"] }
|
tokio = { version = "1", default-features = false, features = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync"] }
|
||||||
|
|
1
Makefile
1
Makefile
|
@ -33,3 +33,4 @@ install: target/release/steamos-manager target/release/steamosctl
|
||||||
|
|
||||||
install -m644 "data/user/com.steampowered.SteamOSManager1.service" "$(DESTDIR)/usr/share/dbus-1/services/"
|
install -m644 "data/user/com.steampowered.SteamOSManager1.service" "$(DESTDIR)/usr/share/dbus-1/services/"
|
||||||
install -m644 "data/user/steamos-manager.service" "$(DESTDIR)/usr/lib/systemd/user/"
|
install -m644 "data/user/steamos-manager.service" "$(DESTDIR)/usr/lib/systemd/user/"
|
||||||
|
install -m644 "data/user/orca.service" "$(DESTDIR)/usr/lib/systemd/user/"
|
||||||
|
|
|
@ -298,6 +298,43 @@
|
||||||
|
|
||||||
</interface>
|
</interface>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
com.steampowered.SteamOSManager1.ScreenReader1
|
||||||
|
@short_description: Optional interface for managing a screen reader.
|
||||||
|
|
||||||
|
This interface is considered unstable and may change between verisons.
|
||||||
|
Once it is considered stable it will be renamed to ScreenReader1
|
||||||
|
-->
|
||||||
|
<interface name="com.steampowered.SteamOSManager1.ScreenReader0">
|
||||||
|
<!--
|
||||||
|
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
|
com.steampowered.SteamOSManager1.Storage1
|
||||||
@short_description: Optional interface for managing storage devices
|
@short_description: Optional interface for managing storage devices
|
||||||
|
|
150
data/test-orca-settings.conf
Normal file
150
data/test-orca-settings.conf
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"orcaModifierKeys": [
|
||||||
|
"Insert",
|
||||||
|
"KP_Insert"
|
||||||
|
],
|
||||||
|
"enableSpeech": true,
|
||||||
|
"onlySpeakDisplayedText": false,
|
||||||
|
"speechServerFactory": "speechdispatcherfactory",
|
||||||
|
"speechServerInfo": null,
|
||||||
|
"voices": {
|
||||||
|
"default": {
|
||||||
|
"established": false
|
||||||
|
},
|
||||||
|
"uppercase": {
|
||||||
|
"average-pitch": 7.0
|
||||||
|
},
|
||||||
|
"hyperlink": {
|
||||||
|
"established": false
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"established": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"speechVerbosityLevel": 1,
|
||||||
|
"readFullRowInGUITable": true,
|
||||||
|
"readFullRowInDocumentTable": true,
|
||||||
|
"readFullRowInSpreadSheet": false,
|
||||||
|
"enableSpeechIndentation": false,
|
||||||
|
"enableEchoByCharacter": false,
|
||||||
|
"enableEchoByWord": false,
|
||||||
|
"enableEchoBySentence": false,
|
||||||
|
"enableKeyEcho": true,
|
||||||
|
"enableAlphabeticKeys": true,
|
||||||
|
"enableNumericKeys": true,
|
||||||
|
"enablePunctuationKeys": true,
|
||||||
|
"enableSpace": true,
|
||||||
|
"enableModifierKeys": true,
|
||||||
|
"enableFunctionKeys": true,
|
||||||
|
"enableActionKeys": true,
|
||||||
|
"enableNavigationKeys": false,
|
||||||
|
"enableDiacriticalKeys": false,
|
||||||
|
"enablePauseBreaks": true,
|
||||||
|
"enableTutorialMessages": true,
|
||||||
|
"enableMnemonicSpeaking": false,
|
||||||
|
"enablePositionSpeaking": false,
|
||||||
|
"enableBraille": true,
|
||||||
|
"disableBrailleEOL": false,
|
||||||
|
"brailleVerbosityLevel": 1,
|
||||||
|
"brailleRolenameStyle": 1,
|
||||||
|
"brailleSelectorIndicator": 192,
|
||||||
|
"brailleLinkIndicator": 192,
|
||||||
|
"enableSound": true,
|
||||||
|
"soundVolume": 0.5,
|
||||||
|
"playSoundForRole": false,
|
||||||
|
"playSoundForState": false,
|
||||||
|
"playSoundForPositionInSet": false,
|
||||||
|
"playSoundForValue": false,
|
||||||
|
"verbalizePunctuationStyle": 1,
|
||||||
|
"presentToolTips": false,
|
||||||
|
"sayAllStyle": 1,
|
||||||
|
"keyboardLayout": 1,
|
||||||
|
"speakBlankLines": true,
|
||||||
|
"speakNumbersAsDigits": false,
|
||||||
|
"speakMisspelledIndicator": true,
|
||||||
|
"textAttributesToSpeak": [],
|
||||||
|
"textAttributesToBraille": [],
|
||||||
|
"textAttributesBrailleIndicator": 0,
|
||||||
|
"profile": [
|
||||||
|
"Default",
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"speakProgressBarUpdates": true,
|
||||||
|
"brailleProgressBarUpdates": false,
|
||||||
|
"beepProgressBarUpdates": false,
|
||||||
|
"progressBarUpdateInterval": 10,
|
||||||
|
"progressBarVerbosity": 1,
|
||||||
|
"ignoreStatusBarProgressBars": true,
|
||||||
|
"enableBrailleWordWrap": false,
|
||||||
|
"enableContractedBraille": false,
|
||||||
|
"brailleContractionTable": "",
|
||||||
|
"enableMouseReview": false,
|
||||||
|
"speakCellCoordinates": true,
|
||||||
|
"speakSpreadsheetCoordinates": true,
|
||||||
|
"alwaysSpeakSelectedSpreadsheetRange": false,
|
||||||
|
"speakCellSpan": true,
|
||||||
|
"speakCellHeaders": true,
|
||||||
|
"skipBlankCells": false,
|
||||||
|
"largeObjectTextLength": 75,
|
||||||
|
"structuralNavigationEnabled": true,
|
||||||
|
"wrappedStructuralNavigation": true,
|
||||||
|
"chatMessageVerbosity": 0,
|
||||||
|
"chatSpeakRoomName": false,
|
||||||
|
"chatAnnounceBuddyTyping": false,
|
||||||
|
"chatRoomHistories": false,
|
||||||
|
"enableFlashMessages": true,
|
||||||
|
"brailleFlashTime": 5000,
|
||||||
|
"flashIsPersistent": false,
|
||||||
|
"flashIsDetailed": true,
|
||||||
|
"messagesAreDetailed": true,
|
||||||
|
"presentDateFormat": "%x",
|
||||||
|
"presentTimeFormat": "%X",
|
||||||
|
"activeProfile": [
|
||||||
|
"Default",
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"startingProfile": [
|
||||||
|
"Default",
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"spellcheckSpellError": true,
|
||||||
|
"spellcheckSpellSuggestion": true,
|
||||||
|
"spellcheckPresentContext": true,
|
||||||
|
"useColorNames": true,
|
||||||
|
"capitalizationStyle": "none",
|
||||||
|
"findResultsVerbosity": 2,
|
||||||
|
"findResultsMinimumLength": 4,
|
||||||
|
"structNavTriggersFocusMode": false,
|
||||||
|
"caretNavTriggersFocusMode": false,
|
||||||
|
"layoutMode": true,
|
||||||
|
"nativeNavTriggersFocusMode": true,
|
||||||
|
"rewindAndFastForwardInSayAll": false,
|
||||||
|
"structNavInSayAll": false,
|
||||||
|
"speakDescription": true,
|
||||||
|
"speakContextBlockquote": true,
|
||||||
|
"speakContextPanel": true,
|
||||||
|
"speakContextLandmark": true,
|
||||||
|
"speakContextNonLandmarkForm": true,
|
||||||
|
"speakContextList": true,
|
||||||
|
"speakContextTable": true,
|
||||||
|
"sayAllContextBlockquote": true,
|
||||||
|
"sayAllContextPanel": true,
|
||||||
|
"sayAllContextLandmark": true,
|
||||||
|
"sayAllContextNonLandmarkForm": true,
|
||||||
|
"sayAllContextList": true,
|
||||||
|
"sayAllContextTable": true
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"default": {
|
||||||
|
"profile": [
|
||||||
|
"Default",
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"pronunciations": {},
|
||||||
|
"keybindings": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pronunciations": {},
|
||||||
|
"keybindings": {}
|
||||||
|
}
|
9
data/user/orca.service
Normal file
9
data/user/orca.service
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Orca Screen Reader
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/orca
|
||||||
|
ExecReload=/usr/bin/orca --replace
|
||||||
|
Restart=on-failure
|
||||||
|
EnvironmentFile=%t/gamescope-environment
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{ArgAction, Parser, Subcommand};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
@ -16,8 +16,8 @@ use steamos_manager::power::{CPUScalingGovernor, GPUPerformanceLevel, GPUPowerPr
|
||||||
use steamos_manager::proxy::{
|
use steamos_manager::proxy::{
|
||||||
AmbientLightSensor1Proxy, BatteryChargeLimit1Proxy, CpuScaling1Proxy, FactoryReset1Proxy,
|
AmbientLightSensor1Proxy, BatteryChargeLimit1Proxy, CpuScaling1Proxy, FactoryReset1Proxy,
|
||||||
FanControl1Proxy, GpuPerformanceLevel1Proxy, GpuPowerProfile1Proxy, HdmiCec1Proxy,
|
FanControl1Proxy, GpuPerformanceLevel1Proxy, GpuPowerProfile1Proxy, HdmiCec1Proxy,
|
||||||
LowPowerMode1Proxy, Manager2Proxy, PerformanceProfile1Proxy, Storage1Proxy, TdpLimit1Proxy,
|
LowPowerMode1Proxy, Manager2Proxy, PerformanceProfile1Proxy, ScreenReader0Proxy, Storage1Proxy,
|
||||||
UpdateBios1Proxy, UpdateDock1Proxy, WifiDebug1Proxy, WifiDebugDump1Proxy,
|
TdpLimit1Proxy, UpdateBios1Proxy, UpdateDock1Proxy, WifiDebug1Proxy, WifiDebugDump1Proxy,
|
||||||
WifiPowerManagement1Proxy,
|
WifiPowerManagement1Proxy,
|
||||||
};
|
};
|
||||||
use steamos_manager::wifi::{WifiBackend, WifiDebugMode, WifiPowerManagement};
|
use steamos_manager::wifi::{WifiBackend, WifiDebugMode, WifiPowerManagement};
|
||||||
|
@ -206,6 +206,42 @@ enum Commands {
|
||||||
|
|
||||||
/// Get the model and variant of this device, if known
|
/// Get the model and variant of this device, if known
|
||||||
GetDeviceModel,
|
GetDeviceModel,
|
||||||
|
|
||||||
|
/// Get whether screen reader is enabled or not.
|
||||||
|
GetScreenReaderEnabled,
|
||||||
|
|
||||||
|
/// Enable or disable the screen reader
|
||||||
|
SetScreenReaderEnabled {
|
||||||
|
#[arg(action = ArgAction::Set, required = true)]
|
||||||
|
enable: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Get screen reader rate
|
||||||
|
GetScreenReaderRate,
|
||||||
|
|
||||||
|
/// Set screen reader rate
|
||||||
|
SetScreenReaderRate {
|
||||||
|
/// Valid rates between 0.0 for slowest and 100.0 for fastest.
|
||||||
|
rate: f64,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Get screen reader pitch
|
||||||
|
GetScreenReaderPitch,
|
||||||
|
|
||||||
|
/// Set screen reader pitch
|
||||||
|
SetScreenReaderPitch {
|
||||||
|
/// Valid pitches between 0.0 for lowest and 10.0 for highest.
|
||||||
|
pitch: f64,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Get screen reader volume
|
||||||
|
GetScreenReaderVolume,
|
||||||
|
|
||||||
|
/// Set screen reader volume
|
||||||
|
SetScreenReaderVolume {
|
||||||
|
/// Valid volume between 0.0 for off, and 10.0 for loudest.
|
||||||
|
volume: f64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_all_properties(conn: &Connection) -> Result<()> {
|
async fn get_all_properties(conn: &Connection) -> Result<()> {
|
||||||
|
@ -519,6 +555,42 @@ async fn main() -> Result<()> {
|
||||||
println!("Model: {device}");
|
println!("Model: {device}");
|
||||||
println!("Variant: {variant}");
|
println!("Variant: {variant}");
|
||||||
}
|
}
|
||||||
|
Commands::GetScreenReaderEnabled => {
|
||||||
|
let proxy = ScreenReader0Proxy::new(&conn).await?;
|
||||||
|
let enabled = proxy.enabled().await?;
|
||||||
|
println!("Enabled: {enabled}");
|
||||||
|
}
|
||||||
|
Commands::SetScreenReaderEnabled { enable } => {
|
||||||
|
let proxy = ScreenReader0Proxy::new(&conn).await?;
|
||||||
|
proxy.set_enabled(*enable).await?;
|
||||||
|
}
|
||||||
|
Commands::GetScreenReaderRate => {
|
||||||
|
let proxy = ScreenReader0Proxy::new(&conn).await?;
|
||||||
|
let rate = proxy.rate().await?;
|
||||||
|
println!("Rate: {rate}");
|
||||||
|
}
|
||||||
|
Commands::SetScreenReaderRate { rate } => {
|
||||||
|
let proxy = ScreenReader0Proxy::new(&conn).await?;
|
||||||
|
proxy.set_rate(*rate).await?;
|
||||||
|
}
|
||||||
|
Commands::GetScreenReaderPitch => {
|
||||||
|
let proxy = ScreenReader0Proxy::new(&conn).await?;
|
||||||
|
let pitch = proxy.pitch().await?;
|
||||||
|
println!("Pitch: {pitch}");
|
||||||
|
}
|
||||||
|
Commands::SetScreenReaderPitch { pitch } => {
|
||||||
|
let proxy = ScreenReader0Proxy::new(&conn).await?;
|
||||||
|
proxy.set_pitch(*pitch).await?;
|
||||||
|
}
|
||||||
|
Commands::GetScreenReaderVolume => {
|
||||||
|
let proxy = ScreenReader0Proxy::new(&conn).await?;
|
||||||
|
let volume = proxy.volume().await?;
|
||||||
|
println!("Volume: {volume}");
|
||||||
|
}
|
||||||
|
Commands::SetScreenReaderVolume { volume } => {
|
||||||
|
let proxy = ScreenReader0Proxy::new(&conn).await?;
|
||||||
|
proxy.set_volume(*volume).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -25,6 +25,7 @@ mod job;
|
||||||
mod manager;
|
mod manager;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod process;
|
mod process;
|
||||||
|
mod screenreader;
|
||||||
mod sls;
|
mod sls;
|
||||||
mod systemd;
|
mod systemd;
|
||||||
mod udev;
|
mod udev;
|
||||||
|
|
|
@ -656,7 +656,7 @@ mod test {
|
||||||
fake_model(SteamDeckVariant::Unknown)
|
fake_model(SteamDeckVariant::Unknown)
|
||||||
.await
|
.await
|
||||||
.expect("fake_model");
|
.expect("fake_model");
|
||||||
assert_eq!(proxy.als_calibration_gain().await.unwrap(), &[]);
|
assert!(proxy.als_calibration_gain().await.unwrap().is_empty());
|
||||||
|
|
||||||
fake_model(SteamDeckVariant::Jupiter)
|
fake_model(SteamDeckVariant::Jupiter)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -31,6 +31,7 @@ use crate::power::{
|
||||||
get_gpu_clocks, get_gpu_clocks_range, get_gpu_performance_level, get_gpu_power_profile,
|
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,
|
get_max_charge_level, get_platform_profile, tdp_limit_manager, TdpManagerCommand,
|
||||||
};
|
};
|
||||||
|
use crate::screenreader::OrcaManager;
|
||||||
use crate::wifi::{
|
use crate::wifi::{
|
||||||
get_wifi_backend, get_wifi_power_management_state, list_wifi_interfaces, WifiBackend,
|
get_wifi_backend, get_wifi_power_management_state, list_wifi_interfaces, WifiBackend,
|
||||||
};
|
};
|
||||||
|
@ -153,6 +154,10 @@ struct PerformanceProfile1 {
|
||||||
tdp_limit_manager: UnboundedSender<TdpManagerCommand>,
|
tdp_limit_manager: UnboundedSender<TdpManagerCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ScreenReader0 {
|
||||||
|
screen_reader: OrcaManager<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
struct Storage1 {
|
struct Storage1 {
|
||||||
proxy: Proxy<'static>,
|
proxy: Proxy<'static>,
|
||||||
job_manager: UnboundedSender<JobManagerCommand>,
|
job_manager: UnboundedSender<JobManagerCommand>,
|
||||||
|
@ -618,6 +623,68 @@ impl PerformanceProfile1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ScreenReader0 {
|
||||||
|
async fn new(connection: &Connection) -> Result<ScreenReader0> {
|
||||||
|
let screen_reader = OrcaManager::new(connection).await?;
|
||||||
|
Ok(ScreenReader0 { screen_reader })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interface(name = "com.steampowered.SteamOSManager1.ScreenReader0")]
|
||||||
|
impl ScreenReader0 {
|
||||||
|
#[zbus(property)]
|
||||||
|
async fn enabled(&self) -> bool {
|
||||||
|
self.screen_reader.enabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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) -> f64 {
|
||||||
|
self.screen_reader.rate()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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) -> f64 {
|
||||||
|
self.screen_reader.pitch()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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) -> f64 {
|
||||||
|
self.screen_reader.volume()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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")]
|
#[interface(name = "com.steampowered.SteamOSManager1.Storage1")]
|
||||||
impl Storage1 {
|
impl Storage1 {
|
||||||
async fn format_device(
|
async fn format_device(
|
||||||
|
@ -919,6 +986,7 @@ pub(crate) async fn create_interfaces(
|
||||||
proxy: proxy.clone(),
|
proxy: proxy.clone(),
|
||||||
channel: daemon,
|
channel: daemon,
|
||||||
};
|
};
|
||||||
|
let screen_reader = ScreenReader0::new(&session).await?;
|
||||||
let wifi_debug = WifiDebug1 {
|
let wifi_debug = WifiDebug1 {
|
||||||
proxy: proxy.clone(),
|
proxy: proxy.clone(),
|
||||||
};
|
};
|
||||||
|
@ -971,6 +1039,8 @@ pub(crate) async fn create_interfaces(
|
||||||
|
|
||||||
object_server.at(MANAGER_PATH, manager2).await?;
|
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 {
|
if steam_deck_variant().await.unwrap_or_default() == SteamDeckVariant::Galileo {
|
||||||
object_server.at(MANAGER_PATH, wifi_debug).await?;
|
object_server.at(MANAGER_PATH, wifi_debug).await?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ mod hdmi_cec1;
|
||||||
mod low_power_mode1;
|
mod low_power_mode1;
|
||||||
mod manager2;
|
mod manager2;
|
||||||
mod performance_profile1;
|
mod performance_profile1;
|
||||||
|
mod screenreader0;
|
||||||
mod storage1;
|
mod storage1;
|
||||||
mod tdp_limit1;
|
mod tdp_limit1;
|
||||||
mod update_bios1;
|
mod update_bios1;
|
||||||
|
@ -43,6 +44,7 @@ pub use crate::proxy::hdmi_cec1::HdmiCec1Proxy;
|
||||||
pub use crate::proxy::low_power_mode1::LowPowerMode1Proxy;
|
pub use crate::proxy::low_power_mode1::LowPowerMode1Proxy;
|
||||||
pub use crate::proxy::manager2::Manager2Proxy;
|
pub use crate::proxy::manager2::Manager2Proxy;
|
||||||
pub use crate::proxy::performance_profile1::PerformanceProfile1Proxy;
|
pub use crate::proxy::performance_profile1::PerformanceProfile1Proxy;
|
||||||
|
pub use crate::proxy::screenreader0::ScreenReader0Proxy;
|
||||||
pub use crate::proxy::storage1::Storage1Proxy;
|
pub use crate::proxy::storage1::Storage1Proxy;
|
||||||
pub use crate::proxy::tdp_limit1::TdpLimit1Proxy;
|
pub use crate::proxy::tdp_limit1::TdpLimit1Proxy;
|
||||||
pub use crate::proxy::update_bios1::UpdateBios1Proxy;
|
pub use crate::proxy::update_bios1::UpdateBios1Proxy;
|
||||||
|
|
45
src/proxy/screenreader0.rs
Normal file
45
src/proxy/screenreader0.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
//! # D-Bus interface proxy for: `com.steampowered.SteamOSManager1.ScreenReader0`
|
||||||
|
//!
|
||||||
|
//! This code was generated by `zbus-xmlgen` `5.1.0` from D-Bus introspection data.
|
||||||
|
//! Source: `com.steampowered.SteamOSManager1.xml`.
|
||||||
|
//!
|
||||||
|
//! You may prefer to adapt it, instead of using it verbatim.
|
||||||
|
//!
|
||||||
|
//! More information can be found in the [Writing a client proxy] section of the zbus
|
||||||
|
//! documentation.
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
|
||||||
|
//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
|
||||||
|
use zbus::proxy;
|
||||||
|
#[proxy(
|
||||||
|
interface = "com.steampowered.SteamOSManager1.ScreenReader0",
|
||||||
|
default_service = "com.steampowered.SteamOSManager1",
|
||||||
|
default_path = "/com/steampowered/SteamOSManager1",
|
||||||
|
assume_defaults = true
|
||||||
|
)]
|
||||||
|
pub trait ScreenReader0 {
|
||||||
|
/// Enabled property
|
||||||
|
#[zbus(property)]
|
||||||
|
fn enabled(&self) -> zbus::Result<bool>;
|
||||||
|
#[zbus(property)]
|
||||||
|
fn set_enabled(&self, value: bool) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
/// Pitch property
|
||||||
|
#[zbus(property)]
|
||||||
|
fn pitch(&self) -> zbus::Result<f64>;
|
||||||
|
#[zbus(property)]
|
||||||
|
fn set_pitch(&self, value: f64) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
/// Rate property
|
||||||
|
#[zbus(property)]
|
||||||
|
fn rate(&self) -> zbus::Result<f64>;
|
||||||
|
#[zbus(property)]
|
||||||
|
fn set_rate(&self, value: f64) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
/// Volume property
|
||||||
|
#[zbus(property)]
|
||||||
|
fn volume(&self) -> zbus::Result<f64>;
|
||||||
|
#[zbus(property)]
|
||||||
|
fn set_volume(&self, value: f64) -> zbus::Result<()>;
|
||||||
|
}
|
403
src/screenreader.rs
Normal file
403
src/screenreader.rs
Normal file
|
@ -0,0 +1,403 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2025 Collabora Ltd.
|
||||||
|
* Copyright © 2025 Valve Software
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail, ensure, Result};
|
||||||
|
use gio::{prelude::SettingsExt, Settings};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::fs::{read_to_string, write};
|
||||||
|
use tracing::{debug, error, info, trace, warn};
|
||||||
|
#[cfg(not(test))]
|
||||||
|
use xdg::BaseDirectories;
|
||||||
|
use zbus::Connection;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::path;
|
||||||
|
use crate::systemd::SystemdUnit;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
const TEST_ORCA_SETTINGS: &str = "data/test-orca-settings.conf";
|
||||||
|
#[cfg(test)]
|
||||||
|
const ORCA_SETTINGS: &str = "orca-settings.conf";
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
const ORCA_SETTINGS: &str = "orca/user-settings.conf";
|
||||||
|
const PITCH_SETTING: &str = "average-pitch";
|
||||||
|
const RATE_SETTING: &str = "rate";
|
||||||
|
const VOLUME_SETTING: &str = "gain";
|
||||||
|
const ENABLE_SETTING: &str = "enableSpeech";
|
||||||
|
|
||||||
|
const A11Y_SETTING: &str = "org.gnome.desktop.a11y.applications";
|
||||||
|
const SCREEN_READER_SETTING: &str = "screen-reader-enabled";
|
||||||
|
|
||||||
|
const PITCH_DEFAULT: f64 = 5.0;
|
||||||
|
const RATE_DEFAULT: f64 = 50.0;
|
||||||
|
const VOLUME_DEFAULT: f64 = 10.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> {
|
||||||
|
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: RATE_DEFAULT,
|
||||||
|
pitch: PITCH_DEFAULT,
|
||||||
|
volume: VOLUME_DEFAULT,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
let _ = manager
|
||||||
|
.load_values()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| warn!("Failed to load orca configuration: {e}"));
|
||||||
|
let a11ysettings = Settings::new(A11Y_SETTING);
|
||||||
|
manager.enabled = a11ysettings.boolean(SCREEN_READER_SETTING);
|
||||||
|
Ok(manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
fn settings_path(&self) -> Result<PathBuf> {
|
||||||
|
let xdg_base = BaseDirectories::new();
|
||||||
|
Ok(xdg_base
|
||||||
|
.get_data_home()
|
||||||
|
.ok_or(anyhow!("No XDG_DATA_HOME found"))?
|
||||||
|
.join(ORCA_SETTINGS))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn settings_path(&self) -> Result<PathBuf> {
|
||||||
|
Ok(path(ORCA_SETTINGS))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_enabled(&mut self, enable: bool) -> Result<()> {
|
||||||
|
if self.enabled == enable {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
{
|
||||||
|
let a11ysettings = Settings::new(A11Y_SETTING);
|
||||||
|
a11ysettings
|
||||||
|
.set_boolean(SCREEN_READER_SETTING, enable)
|
||||||
|
.map_err(|e| anyhow!("Unable to set screen reader enabled gsetting, {e}"))?;
|
||||||
|
}
|
||||||
|
self.set_orca_enabled(enable).await?;
|
||||||
|
if enable {
|
||||||
|
self.restart_orca().await?;
|
||||||
|
} else {
|
||||||
|
self.stop_orca().await?;
|
||||||
|
}
|
||||||
|
self.enabled = enable;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pitch(&self) -> f64 {
|
||||||
|
self.pitch
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_pitch(&mut self, pitch: f64) -> Result<()> {
|
||||||
|
trace!("set_pitch called with {pitch}");
|
||||||
|
|
||||||
|
self.set_orca_option(PITCH_SETTING, pitch).await?;
|
||||||
|
self.pitch = pitch;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rate(&self) -> f64 {
|
||||||
|
self.rate
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_rate(&mut self, rate: f64) -> Result<()> {
|
||||||
|
trace!("set_rate called with {rate}");
|
||||||
|
|
||||||
|
self.set_orca_option(RATE_SETTING, rate).await?;
|
||||||
|
self.rate = rate;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn volume(&self) -> f64 {
|
||||||
|
self.volume
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_volume(&mut self, volume: f64) -> Result<()> {
|
||||||
|
trace!("set_volume called with {volume}");
|
||||||
|
|
||||||
|
self.set_orca_option(VOLUME_SETTING, volume).await?;
|
||||||
|
self.volume = volume;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_orca_enabled(&mut self, enabled: bool) -> Result<()> {
|
||||||
|
// Change json file
|
||||||
|
let data = read_to_string(self.settings_path()?).await?;
|
||||||
|
let mut json: Value = serde_json::from_str(&data)?;
|
||||||
|
|
||||||
|
let general = json
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or(anyhow!("orca user-settings.conf json is not an object"))?
|
||||||
|
.entry("general")
|
||||||
|
.or_insert(Value::Object(Map::new()));
|
||||||
|
general
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or(anyhow!("orca user-settings.conf general is not an object"))?
|
||||||
|
.insert(ENABLE_SETTING.to_string(), Value::Bool(enabled));
|
||||||
|
|
||||||
|
let data = serde_json::to_string_pretty(&json)?;
|
||||||
|
Ok(write(self.settings_path()?, data.as_bytes()).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_values(&mut self) -> Result<()> {
|
||||||
|
debug!("Loading orca values from user-settings.conf");
|
||||||
|
let data = read_to_string(self.settings_path()?).await?;
|
||||||
|
let json: Value = serde_json::from_str(&data)?;
|
||||||
|
|
||||||
|
let Some(default_voice) = json
|
||||||
|
.get("profiles")
|
||||||
|
.and_then(|profiles| profiles.get("default"))
|
||||||
|
.and_then(|default_profile| default_profile.get("voices"))
|
||||||
|
.and_then(|voices| voices.get("default"))
|
||||||
|
else {
|
||||||
|
warn!("Orca user-settings.conf missing default voice");
|
||||||
|
self.pitch = PITCH_DEFAULT;
|
||||||
|
self.rate = RATE_DEFAULT;
|
||||||
|
self.volume = VOLUME_DEFAULT;
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
if let Some(pitch) = default_voice.get(PITCH_SETTING) {
|
||||||
|
self.pitch = pitch.as_f64().unwrap_or_else(|| {
|
||||||
|
error!("Unable to convert orca pitch setting to float value");
|
||||||
|
PITCH_DEFAULT
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
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!(
|
||||||
|
"Done loading orca user-settings.conf, values: Rate: {}, Pitch: {}, Volume: {}",
|
||||||
|
self.rate, self.pitch, self.volume
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_orca_option(&self, option: &str, value: f64) -> Result<()> {
|
||||||
|
if let Some(range) = VALID_SETTINGS.get(option) {
|
||||||
|
ensure!(
|
||||||
|
range.contains(&value),
|
||||||
|
"orca option {option} value {value} out of range"
|
||||||
|
);
|
||||||
|
} 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 profiles = json
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or(anyhow!("orca user-settings.conf json is not an object"))?
|
||||||
|
.entry("profiles")
|
||||||
|
.or_insert(Value::Object(Map::new()));
|
||||||
|
let default_profile = profiles
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or(anyhow!("orca user-settings.conf profiles is not an object"))?
|
||||||
|
.entry("default")
|
||||||
|
.or_insert(Value::Object(Map::new()));
|
||||||
|
let voices = default_profile
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or(anyhow!(
|
||||||
|
"orca user-settings.conf default profile is not an object"
|
||||||
|
))?
|
||||||
|
.entry("voices")
|
||||||
|
.or_insert(Value::Object(Map::new()));
|
||||||
|
let default_voice = voices
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or(anyhow!("orca user-settings.conf voices is not an object"))?
|
||||||
|
.entry("default")
|
||||||
|
.or_insert(Value::Object(Map::new()));
|
||||||
|
default_voice
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or(anyhow!(
|
||||||
|
"orca user-settings.conf default voice is not an object"
|
||||||
|
))?
|
||||||
|
.insert(option.to_string(), value.into());
|
||||||
|
|
||||||
|
let data = serde_json::to_string_pretty(&json)?;
|
||||||
|
Ok(write(self.settings_path()?, data.as_bytes()).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
async fn restart_orca(&self) -> Result<()> {
|
||||||
|
trace!("Restarting orca...");
|
||||||
|
self.orca_unit.restart().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
async fn restart_orca(&self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
async fn stop_orca(&self) -> Result<()> {
|
||||||
|
trace!("Stopping orca...");
|
||||||
|
self.orca_unit.stop().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
async fn stop_orca(&self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::testing;
|
||||||
|
use tokio::fs::{copy, remove_file};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_enable_disable() {
|
||||||
|
let mut h = testing::start();
|
||||||
|
copy(TEST_ORCA_SETTINGS, h.test.path().join(ORCA_SETTINGS))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut manager = OrcaManager::new(&h.new_dbus().await.expect("new_dbus"))
|
||||||
|
.await
|
||||||
|
.expect("OrcaManager::new");
|
||||||
|
let enable_result = manager.set_enabled(true).await;
|
||||||
|
assert!(enable_result.is_ok());
|
||||||
|
assert_eq!(manager.enabled(), true);
|
||||||
|
|
||||||
|
let disable_result = manager.set_enabled(false).await;
|
||||||
|
assert!(disable_result.is_ok());
|
||||||
|
assert_eq!(manager.enabled(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_pitch() {
|
||||||
|
let mut h = testing::start();
|
||||||
|
copy(TEST_ORCA_SETTINGS, h.test.path().join(ORCA_SETTINGS))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut manager = OrcaManager::new(&h.new_dbus().await.expect("new_dbus"))
|
||||||
|
.await
|
||||||
|
.expect("OrcaManager::new");
|
||||||
|
let set_result = manager.set_pitch(5.0).await;
|
||||||
|
assert!(set_result.is_ok());
|
||||||
|
assert_eq!(manager.pitch(), 5.0);
|
||||||
|
|
||||||
|
let too_low_result = manager.set_pitch(-1.0).await;
|
||||||
|
assert!(too_low_result.is_err());
|
||||||
|
assert_eq!(manager.pitch(), 5.0);
|
||||||
|
|
||||||
|
let too_high_result = manager.set_pitch(12.0).await;
|
||||||
|
assert!(too_high_result.is_err());
|
||||||
|
assert_eq!(manager.pitch(), 5.0);
|
||||||
|
|
||||||
|
remove_file(h.test.path().join(ORCA_SETTINGS))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let nofile_result = manager.set_pitch(7.0).await;
|
||||||
|
assert_eq!(manager.pitch(), 5.0);
|
||||||
|
assert!(nofile_result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rate() {
|
||||||
|
let mut h = testing::start();
|
||||||
|
copy(TEST_ORCA_SETTINGS, h.test.path().join(ORCA_SETTINGS))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut manager = OrcaManager::new(&h.new_dbus().await.expect("new_dbus"))
|
||||||
|
.await
|
||||||
|
.expect("OrcaManager::new");
|
||||||
|
let set_result = manager.set_rate(5.0).await;
|
||||||
|
assert!(set_result.is_ok());
|
||||||
|
assert_eq!(manager.rate(), 5.0);
|
||||||
|
|
||||||
|
let too_low_result = manager.set_rate(-1.0).await;
|
||||||
|
assert!(too_low_result.is_err());
|
||||||
|
assert_eq!(manager.rate(), 5.0);
|
||||||
|
|
||||||
|
let too_high_result = manager.set_rate(101.0).await;
|
||||||
|
assert!(too_high_result.is_err());
|
||||||
|
assert_eq!(manager.rate(), 5.0);
|
||||||
|
|
||||||
|
remove_file(h.test.path().join(ORCA_SETTINGS))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let nofile_result = manager.set_rate(7.0).await;
|
||||||
|
assert_eq!(manager.rate(), 5.0);
|
||||||
|
assert!(nofile_result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_volume() {
|
||||||
|
let mut h = testing::start();
|
||||||
|
copy(TEST_ORCA_SETTINGS, h.test.path().join(ORCA_SETTINGS))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut manager = OrcaManager::new(&h.new_dbus().await.expect("new_dbus"))
|
||||||
|
.await
|
||||||
|
.expect("OrcaManager::new");
|
||||||
|
let set_result = manager.set_volume(5.0).await;
|
||||||
|
assert!(set_result.is_ok());
|
||||||
|
assert_eq!(manager.volume(), 5.0);
|
||||||
|
|
||||||
|
let too_low_result = manager.set_volume(-1.0).await;
|
||||||
|
assert!(too_low_result.is_err());
|
||||||
|
assert_eq!(manager.volume(), 5.0);
|
||||||
|
|
||||||
|
let too_high_result = manager.set_volume(12.0).await;
|
||||||
|
assert!(too_high_result.is_err());
|
||||||
|
assert_eq!(manager.volume(), 5.0);
|
||||||
|
|
||||||
|
remove_file(h.test.path().join(ORCA_SETTINGS))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let nofile_result = manager.set_volume(7.0).await;
|
||||||
|
assert_eq!(manager.volume(), 5.0);
|
||||||
|
assert!(nofile_result.is_err());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue