feat(sound): redesign with separate device profiles page (#1500)
This commit is contained in:
parent
6ebc2208ed
commit
2c9f60cd5f
65 changed files with 3179 additions and 1971 deletions
327
Cargo.lock
generated
327
Cargo.lock
generated
|
|
@ -228,12 +228,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "annotate-snippets"
|
||||
version = "0.9.2"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e"
|
||||
checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"yansi-term",
|
||||
"anstyle",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -743,21 +743,19 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.69.5"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"annotate-snippets",
|
||||
"bitflags 2.10.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.12.1",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"itertools 0.13.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash 1.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"shlex",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
|
@ -1132,7 +1130,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9acd0bdbbf4b2612d09f52ba61da432140cb10930354079d0d53fafc12968726"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon 0.13.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1302,7 +1310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1422,9 +1430,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
|
@ -1434,9 +1442,6 @@ name = "cookie-factory"
|
|||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2"
|
||||
dependencies = [
|
||||
"futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
|
|
@ -1527,7 +1532,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-config"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"atomicwrites",
|
||||
"cosmic-config-derive",
|
||||
|
|
@ -1548,7 +1553,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-config-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
|
|
@ -1622,6 +1627,19 @@ dependencies = [
|
|||
"xdg-shell-wrapper-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-pipewire"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"intmap",
|
||||
"libspa",
|
||||
"libspa-sys",
|
||||
"pipewire",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-protocols"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1665,7 +1683,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"accounts-zbus",
|
||||
"anyhow",
|
||||
|
|
@ -1747,7 +1765,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-a11y-manager-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"cosmic-protocols",
|
||||
"iced_futures",
|
||||
|
|
@ -1760,7 +1778,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-accessibility-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"cosmic-dbus-a11y",
|
||||
"futures",
|
||||
|
|
@ -1772,7 +1790,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-airplane-mode-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"iced_futures",
|
||||
|
|
@ -1783,7 +1801,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-bluetooth-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"bluez-zbus",
|
||||
"futures",
|
||||
|
|
@ -1825,7 +1843,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-daemon-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"iced_futures",
|
||||
|
|
@ -1837,7 +1855,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-network-manager-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"cosmic-dbus-networkmanager",
|
||||
"futures",
|
||||
|
|
@ -1852,7 +1870,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-page"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"derive_setters",
|
||||
"downcast-rs 2.0.2",
|
||||
|
|
@ -1865,15 +1883,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-sound-subscription"
|
||||
version = "1.0.0-beta1"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"async-fn-stream",
|
||||
"cosmic-pipewire",
|
||||
"crossbeam-queue",
|
||||
"futures",
|
||||
"indexmap 2.12.0",
|
||||
"intmap",
|
||||
"libcosmic",
|
||||
"libpulse-binding",
|
||||
"log",
|
||||
"pipewire",
|
||||
"numtoa",
|
||||
"rustix 1.1.2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
|
|
@ -1881,7 +1899,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-system"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"byte-unit",
|
||||
|
|
@ -1893,7 +1911,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-upower-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"iced_futures",
|
||||
|
|
@ -1906,7 +1924,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cosmic-settings-wallpaper"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
dependencies = [
|
||||
"cosmic-bg-config",
|
||||
"cosmic-randr-shell",
|
||||
|
|
@ -1949,7 +1967,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-theme"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"almost",
|
||||
"cosmic-config",
|
||||
|
|
@ -2008,6 +2026,15 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
|
|
@ -3385,7 +3412,7 @@ dependencies = [
|
|||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.62.2",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3400,7 +3427,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced"
|
||||
version = "0.14.0-dev"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_accessibility",
|
||||
|
|
@ -3418,7 +3445,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_accessibility"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_winit",
|
||||
|
|
@ -3427,7 +3454,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_core"
|
||||
version = "0.14.0-dev"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bytes",
|
||||
|
|
@ -3452,7 +3479,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_futures"
|
||||
version = "0.14.0-dev"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"iced_core",
|
||||
|
|
@ -3478,7 +3505,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_graphics"
|
||||
version = "0.14.0-dev"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bytemuck",
|
||||
|
|
@ -3500,7 +3527,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_renderer"
|
||||
version = "0.14.0-dev"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"iced_graphics",
|
||||
"iced_tiny_skia",
|
||||
|
|
@ -3512,7 +3539,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_runtime"
|
||||
version = "0.14.0-dev"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cosmic-client-toolkit",
|
||||
|
|
@ -3528,7 +3555,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_tiny_skia"
|
||||
version = "0.14.0-dev"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cosmic-text",
|
||||
|
|
@ -3544,7 +3571,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_wgpu"
|
||||
version = "0.14.0-dev"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"bitflags 2.10.0",
|
||||
|
|
@ -3575,7 +3602,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_widget"
|
||||
version = "0.14.0-dev"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"cosmic-client-toolkit",
|
||||
"dnd",
|
||||
|
|
@ -3596,7 +3623,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_winit"
|
||||
version = "0.14.0-dev"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"cosmic-client-toolkit",
|
||||
"dnd",
|
||||
|
|
@ -4236,6 +4263,12 @@ dependencies = [
|
|||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intmap"
|
||||
version = "3.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2e611826a1868311677fdcdfbec9e8621d104c732d080f546a854530232f0ee"
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.11"
|
||||
|
|
@ -4262,6 +4295,15 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
|
|
@ -4604,12 +4646,6 @@ version = "1.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.3"
|
||||
|
|
@ -4625,7 +4661,7 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
|||
[[package]]
|
||||
name = "libcosmic"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#2c93a4094fe331726ad0f7b80ae00a28f856543b"
|
||||
source = "git+https://github.com/pop-os/libcosmic#fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143"
|
||||
dependencies = [
|
||||
"apply",
|
||||
"ashpd 0.12.0",
|
||||
|
|
@ -4694,33 +4730,6 @@ version = "0.2.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "libpulse-binding"
|
||||
version = "2.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"libc",
|
||||
"libpulse-sys",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libpulse-sys"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d74371848b22e989f829cc1621d2ebd74960711557d8b45cfe740f60d0a05e61"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"pkg-config",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.10"
|
||||
|
|
@ -4734,9 +4743,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libspa"
|
||||
version = "0.8.0"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810"
|
||||
checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cc",
|
||||
|
|
@ -4744,20 +4753,20 @@ dependencies = [
|
|||
"cookie-factory",
|
||||
"libc",
|
||||
"libspa-sys",
|
||||
"nix 0.27.1",
|
||||
"nom 7.1.3",
|
||||
"system-deps",
|
||||
"nix 0.30.1",
|
||||
"nom 8.0.0",
|
||||
"system-deps 7.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libspa-sys"
|
||||
version = "0.8.0"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f"
|
||||
checksum = "901049455d2eb6decf9058235d745237952f4804bc584c5fcb41412e6adcc6e0"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"system-deps",
|
||||
"system-deps 7.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5042,7 +5051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5193,17 +5202,6 @@ dependencies = [
|
|||
"memoffset 0.7.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
|
|
@ -5426,6 +5424,12 @@ dependencies = [
|
|||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numtoa"
|
||||
version = "1.0.0-alpha1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3f98606e662e333dada0fa9fb6723a3c363fb4a66b51e47ce964cfaf58833d2"
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
|
|
@ -5986,30 +5990,30 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pipewire"
|
||||
version = "0.8.0"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda"
|
||||
checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.10.0",
|
||||
"libc",
|
||||
"libspa",
|
||||
"libspa-sys",
|
||||
"nix 0.27.1",
|
||||
"nix 0.30.1",
|
||||
"once_cell",
|
||||
"pipewire-sys",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pipewire-sys"
|
||||
version = "0.8.0"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112"
|
||||
checksum = "cb028afee0d6ca17020b090e3b8fa2d7de23305aef975c7e5192a5050246ea36"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"libspa-sys",
|
||||
"system-deps",
|
||||
"system-deps 7.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -6419,7 +6423,7 @@ dependencies = [
|
|||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"simd_helpers",
|
||||
"system-deps",
|
||||
"system-deps 6.2.2",
|
||||
"thiserror 1.0.69",
|
||||
"v_frame",
|
||||
"wasm-bindgen",
|
||||
|
|
@ -6959,6 +6963,15 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.15.1"
|
||||
|
|
@ -7422,13 +7435,26 @@ version = "6.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"cfg-expr 0.15.8",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.8.23",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "7.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f"
|
||||
dependencies = [
|
||||
"cfg-expr 0.20.4",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.9.8",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tachyonix"
|
||||
version = "0.3.1"
|
||||
|
|
@ -7466,6 +7492,12 @@ version = "0.12.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
|
||||
|
||||
[[package]]
|
||||
name = "temp-dir"
|
||||
version = "0.1.16"
|
||||
|
|
@ -7716,11 +7748,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_edit 0.22.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
|
||||
dependencies = [
|
||||
"indexmap 2.12.0",
|
||||
"serde_core",
|
||||
"serde_spanned 1.0.3",
|
||||
"toml_datetime 0.7.3",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 0.7.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
|
|
@ -7758,7 +7805,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
|||
dependencies = [
|
||||
"indexmap 2.12.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"winnow 0.7.13",
|
||||
]
|
||||
|
|
@ -7784,6 +7831,12 @@ dependencies = [
|
|||
"winnow 0.7.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
|
|
@ -7994,6 +8047,12 @@ version = "0.1.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
|
|
@ -8639,20 +8698,7 @@ dependencies = [
|
|||
"windows-interface 0.59.3",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -8750,15 +8796,6 @@ dependencies = [
|
|||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
|
|
@ -8768,15 +8805,6 @@ dependencies = [
|
|||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
|
|
@ -9338,15 +9366,6 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yansi-term"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yazi"
|
||||
version = "0.2.1"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[workspace]
|
||||
members = ["cosmic-settings", "page", "pages/*", "subscriptions/*"]
|
||||
members = ["cosmic-settings", "crates/*", "page", "pages/*", "subscriptions/*"]
|
||||
default-members = ["cosmic-settings"]
|
||||
resolver = "3"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-only"
|
||||
publish = false
|
||||
|
|
@ -72,7 +72,7 @@ tachyonix = "0.3.1"
|
|||
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
tokio = { workspace = true, features = ["fs", "io-util", "sync"] }
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.20"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||
udev = { version = "0.9.3", optional = true }
|
||||
upower_dbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
bluez-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
|
|
|
|||
|
|
@ -550,6 +550,13 @@ impl cosmic::Application for SettingsApp {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "page-sound")]
|
||||
crate::pages::Message::SoundDeviceProfiles(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<sound::device_profiles::Page>() {
|
||||
return page.update(message).map(Into::into);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::StartupApps(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<applications::startup_apps::Page>() {
|
||||
return page.update(message).map(Into::into);
|
||||
|
|
|
|||
|
|
@ -201,11 +201,6 @@ fn init_localizer() {
|
|||
}
|
||||
|
||||
fn init_logger() {
|
||||
let log_level = std::env::var("RUST_LOG")
|
||||
.ok()
|
||||
.and_then(|level| level.parse::<tracing::Level>().ok())
|
||||
.unwrap_or(tracing::Level::INFO);
|
||||
|
||||
let log_format = tracing_subscriber::fmt::format()
|
||||
.pretty()
|
||||
.without_time()
|
||||
|
|
@ -214,17 +209,14 @@ fn init_logger() {
|
|||
.with_target(false)
|
||||
.with_thread_names(true);
|
||||
|
||||
let log_filter = tracing_subscriber::fmt::Layer::default()
|
||||
let log_layer = tracing_subscriber::fmt::Layer::default()
|
||||
.with_writer(std::io::stderr)
|
||||
.event_format(log_format)
|
||||
.with_filter(tracing_subscriber::filter::filter_fn(move |metadata| {
|
||||
let target = metadata.target();
|
||||
metadata.level() == &tracing::Level::ERROR
|
||||
|| ((target.starts_with("cosmic_settings") || target.starts_with("cosmic_bg"))
|
||||
&& metadata.level() <= &log_level)
|
||||
}));
|
||||
.event_format(log_format);
|
||||
|
||||
tracing_subscriber::registry().with(log_filter).init();
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::from_env("RUST_LOG"))
|
||||
.with(log_layer)
|
||||
.init();
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ pub enum Message {
|
|||
Region(time::region::Message),
|
||||
#[cfg(feature = "page-sound")]
|
||||
Sound(sound::Message),
|
||||
#[cfg(feature = "page-sound")]
|
||||
SoundDeviceProfiles(sound::device_profiles::Message),
|
||||
StartupApps(applications::startup_apps::Message),
|
||||
#[cfg(feature = "page-users")]
|
||||
User(system::users::Message),
|
||||
|
|
|
|||
96
cosmic-settings/src/pages/sound/device_profiles.rs
Normal file
96
cosmic-settings/src/pages/sound/device_profiles.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{Apply, widget};
|
||||
use cosmic_settings_page::{self as page, Section, section};
|
||||
use cosmic_settings_sound_subscription::{self as subscription};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {}
|
||||
|
||||
impl From<Message> for crate::pages::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::SoundDeviceProfiles(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for crate::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::Message::PageMessage(message.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page {
|
||||
entity: page::Entity,
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("sound-device-profiles", "preferences-sound-symbolic")
|
||||
.title(fl!("sound-device-profiles"))
|
||||
}
|
||||
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(view())])
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> cosmic::Task<crate::pages::Message> {
|
||||
cosmic::Task::done(crate::pages::Message::Sound(super::Message::Reload))
|
||||
}
|
||||
|
||||
fn set_id(&mut self, entity: cosmic_settings_page::Entity) {
|
||||
self.entity = entity;
|
||||
}
|
||||
|
||||
fn subscription(
|
||||
&self,
|
||||
_core: &cosmic::Core,
|
||||
) -> cosmic::iced::Subscription<crate::pages::Message> {
|
||||
cosmic::iced::Subscription::run(subscription::watch)
|
||||
.map(|message| super::Message::Subscription(message).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, _message: Message) -> cosmic::Task<crate::app::Message> {
|
||||
cosmic::Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view() -> Section<crate::pages::Message> {
|
||||
Section::default().view::<Page>(move |binder, _page, _section| {
|
||||
let sound_page_id = binder.find_page_by_id("sound").unwrap().0;
|
||||
let sound_page = binder.page[sound_page_id]
|
||||
.downcast_ref::<super::Page>()
|
||||
.unwrap();
|
||||
|
||||
let devices = sound_page
|
||||
.model
|
||||
.device_profile_dropdowns
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|(device_id, name, active_profile, indexes, descriptions)| {
|
||||
let dropdown = widget::dropdown::popup_dropdown(
|
||||
descriptions,
|
||||
active_profile,
|
||||
move |id| super::Message::SetProfile(device_id, indexes[id]),
|
||||
cosmic::iced::window::Id::RESERVED,
|
||||
super::Message::Surface,
|
||||
crate::Message::from,
|
||||
)
|
||||
.apply(cosmic::Element::from)
|
||||
.map(crate::pages::Message::from);
|
||||
|
||||
widget::settings::item::builder(name).control(dropdown)
|
||||
});
|
||||
|
||||
widget::settings::section().extend(devices).into()
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod device_profiles;
|
||||
|
||||
use cosmic::{
|
||||
Apply, Element, Task,
|
||||
iced::{Alignment, Length, window},
|
||||
|
|
@ -9,43 +11,42 @@ use cosmic::{
|
|||
};
|
||||
use cosmic_config::{Config, ConfigGet, ConfigSet};
|
||||
use cosmic_settings_page::{self as page, Section, section};
|
||||
use cosmic_settings_sound_subscription as subscription;
|
||||
use slab::Slab;
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use cosmic_settings_sound_subscription as subscription;
|
||||
|
||||
const AUDIO_CONFIG: &str = "com.system76.CosmicAudio";
|
||||
const AMPLIFICATION_SINK: &str = "amplification_sink";
|
||||
const AMPLIFICATION_SOURCE: &str = "amplification_source";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
/// Change the balance of the active sink.
|
||||
SinkBalanceChanged(u32),
|
||||
/// Reload the model
|
||||
Reload,
|
||||
/// Change the default output.
|
||||
SinkChanged(usize),
|
||||
/// Toggle the mute status of the output.
|
||||
SinkMuteToggle,
|
||||
/// Change the active profile for an output.
|
||||
SinkProfileChanged(usize),
|
||||
/// Request to change the default output volume.
|
||||
SinkVolumeChanged(u32),
|
||||
/// Toggle amplification for sink
|
||||
ToggleOverAmplificationSink(bool),
|
||||
SetDefaultSink(usize),
|
||||
/// Change the default input output.
|
||||
SourceChanged(usize),
|
||||
/// Toggle the mute status of the input output.
|
||||
SourceMuteToggle,
|
||||
/// Change the active profile for an output.
|
||||
SourceProfileChanged(usize),
|
||||
SetDefaultSource(usize),
|
||||
/// Set the profile of a sound device.
|
||||
SetProfile(u32, u32),
|
||||
/// Change the balance of the active sink.
|
||||
SetSinkBalance(u32),
|
||||
/// Request to change the default output volume.
|
||||
SetSinkVolume(u32),
|
||||
/// Request to change the input volume.
|
||||
SourceVolumeChanged(u32),
|
||||
/// Toggle amplification for sink
|
||||
ToggleOverAmplificationSource(bool),
|
||||
SetSourceVolume(u32),
|
||||
/// Messages handled by the sound module in cosmic-settings-subscriptions
|
||||
Subscription(subscription::Message),
|
||||
/// Surface Action
|
||||
Surface(surface::Action),
|
||||
/// Toggle the mute status of the output.
|
||||
ToggleSinkMute,
|
||||
/// Toggle the mute status of the input output.
|
||||
ToggleSourceMute,
|
||||
/// Toggle amplification for sink
|
||||
ToggleOverAmplificationSink(bool),
|
||||
/// Toggle amplification for sink
|
||||
ToggleOverAmplificationSource(bool),
|
||||
}
|
||||
|
||||
impl From<Message> for crate::pages::Message {
|
||||
|
|
@ -66,15 +67,32 @@ impl From<subscription::Message> for Message {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page {
|
||||
entity: page::Entity,
|
||||
model: subscription::Model,
|
||||
device_profiles: page::Entity,
|
||||
pub(self) model: subscription::Model,
|
||||
sound_config: Option<Config>,
|
||||
amplification_sink: bool,
|
||||
amplification_source: bool,
|
||||
}
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
let mut model = subscription::Model::default();
|
||||
model.unplugged_text = fl!("sound-device-port-unplugged");
|
||||
model.hd_audio_text = fl!("sound-hd-audio");
|
||||
model.usb_audio_text = fl!("sound-usb-audio");
|
||||
Self {
|
||||
entity: page::Entity::default(),
|
||||
device_profiles: page::Entity::default(),
|
||||
model,
|
||||
sound_config: None,
|
||||
amplification_sink: false,
|
||||
amplification_source: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
|
||||
match Config::new(AUDIO_CONFIG, 1) {
|
||||
|
|
@ -97,7 +115,11 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(output()), sections.insert(input())])
|
||||
Some(vec![
|
||||
sections.insert(output()),
|
||||
sections.insert(input()),
|
||||
sections.insert(device_profiles()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
|
|
@ -106,6 +128,10 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
.description(fl!("sound", "desc"))
|
||||
}
|
||||
|
||||
fn set_id(&mut self, entity: page::Entity) {
|
||||
self.entity = entity;
|
||||
}
|
||||
|
||||
fn subscription(
|
||||
&self,
|
||||
_core: &cosmic::Core,
|
||||
|
|
@ -115,10 +141,9 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
}
|
||||
|
||||
fn on_leave(&mut self) -> Task<crate::pages::Message> {
|
||||
self.model.clear();
|
||||
|
||||
*self = Page {
|
||||
entity: self.entity,
|
||||
device_profiles: self.device_profiles,
|
||||
..Page::default()
|
||||
};
|
||||
|
||||
|
|
@ -126,37 +151,65 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(
|
||||
mut page: page::Insert<crate::pages::Message>,
|
||||
) -> page::Insert<crate::pages::Message> {
|
||||
let id = page.sub_page_with_id::<device_profiles::Page>();
|
||||
let model = page.model.page_mut::<Page>().unwrap();
|
||||
model.device_profiles = id;
|
||||
page
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) -> Task<crate::app::Message> {
|
||||
match message {
|
||||
Message::SinkBalanceChanged(balance) => {
|
||||
Message::Surface(a) => return cosmic::task::message(crate::app::Message::Surface(a)),
|
||||
|
||||
Message::Subscription(message) => {
|
||||
return self
|
||||
.model
|
||||
.sink_balance_changed(balance)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
Message::SinkChanged(pos) => {
|
||||
return self
|
||||
.model
|
||||
.sink_changed(pos)
|
||||
.update(message)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::SinkMuteToggle => self.model.sink_mute_toggle(),
|
||||
|
||||
Message::SinkProfileChanged(profile) => {
|
||||
Message::SetSinkBalance(balance) => {
|
||||
return self
|
||||
.model
|
||||
.sink_profile_changed(profile)
|
||||
.set_sink_balance(balance)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::SinkVolumeChanged(volume) => {
|
||||
Message::SetDefaultSink(pos) => {
|
||||
return self
|
||||
.model
|
||||
.sink_volume_changed(volume)
|
||||
.set_default_sink(pos)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::SetDefaultSource(pos) => {
|
||||
return self
|
||||
.model
|
||||
.set_default_source(pos)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::ToggleSinkMute => self.model.toggle_sink_mute(),
|
||||
|
||||
Message::ToggleSourceMute => self.model.toggle_source_mute(),
|
||||
|
||||
Message::SetSinkVolume(volume) => {
|
||||
return self
|
||||
.model
|
||||
.set_sink_volume(volume)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::SetSourceVolume(volume) => {
|
||||
return self
|
||||
.model
|
||||
.set_source_volume(volume)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
|
|
@ -170,29 +223,6 @@ impl Page {
|
|||
}
|
||||
}
|
||||
|
||||
Message::SourceChanged(pos) => {
|
||||
return self
|
||||
.model
|
||||
.source_changed(pos)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::SourceMuteToggle => self.model.source_mute_toggle(),
|
||||
|
||||
Message::SourceProfileChanged(profile) => {
|
||||
return self
|
||||
.model
|
||||
.source_profile_changed(profile)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::SourceVolumeChanged(volume) => {
|
||||
return self
|
||||
.model
|
||||
.source_volume_changed(volume)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::ToggleOverAmplificationSource(enabled) => {
|
||||
self.amplification_source = enabled;
|
||||
|
||||
|
|
@ -203,14 +233,17 @@ impl Page {
|
|||
}
|
||||
}
|
||||
|
||||
Message::Subscription(message) => {
|
||||
return self
|
||||
.model
|
||||
.update(message)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
Message::SetProfile(object_id, index) => {
|
||||
self.model.set_profile(object_id, index, true);
|
||||
}
|
||||
|
||||
Message::Surface(a) => return cosmic::task::message(crate::app::Message::Surface(a)),
|
||||
Message::Reload => {
|
||||
let mut model = subscription::Model::default();
|
||||
model.hd_audio_text = std::mem::take(&mut self.model.hd_audio_text);
|
||||
model.unplugged_text = std::mem::take(&mut self.model.unplugged_text);
|
||||
model.usb_audio_text = std::mem::take(&mut self.model.usb_audio_text);
|
||||
self.model = model;
|
||||
}
|
||||
}
|
||||
|
||||
Task::none()
|
||||
|
|
@ -223,7 +256,6 @@ fn input() -> Section<crate::pages::Message> {
|
|||
let volume = descriptions.insert(fl!("sound-input", "volume"));
|
||||
let device = descriptions.insert(fl!("sound-input", "device"));
|
||||
let _level = descriptions.insert(fl!("sound-input", "level"));
|
||||
let profile = descriptions.insert(fl!("profile"));
|
||||
let amplification = descriptions.insert(fl!("amplification"));
|
||||
let amplification_desc = descriptions.insert(fl!("amplification", "desc"));
|
||||
|
||||
|
|
@ -237,12 +269,12 @@ fn input() -> Section<crate::pages::Message> {
|
|||
|
||||
let slider = if page.amplification_source {
|
||||
widget::slider(0..=150, page.model.source_volume, |change| {
|
||||
Message::SourceVolumeChanged(change).into()
|
||||
Message::SetSourceVolume(change).into()
|
||||
})
|
||||
.breakpoints(&[100])
|
||||
} else {
|
||||
widget::slider(0..=100, page.model.source_volume, |change| {
|
||||
Message::SourceVolumeChanged(change).into()
|
||||
Message::SetSourceVolume(change).into()
|
||||
})
|
||||
};
|
||||
|
||||
|
|
@ -254,7 +286,7 @@ fn input() -> Section<crate::pages::Message> {
|
|||
} else {
|
||||
"audio-input-microphone-symbolic"
|
||||
}))
|
||||
.on_press(Message::SourceMuteToggle.into()),
|
||||
.on_press(Message::ToggleSourceMute.into()),
|
||||
)
|
||||
.push(
|
||||
widget::text::body(&page.model.source_volume_text)
|
||||
|
|
@ -266,7 +298,7 @@ fn input() -> Section<crate::pages::Message> {
|
|||
let devices = widget::dropdown::popup_dropdown(
|
||||
page.model.sources(),
|
||||
Some(page.model.active_source().unwrap_or(0)),
|
||||
Message::SourceChanged,
|
||||
Message::SetDefaultSource,
|
||||
window::Id::RESERVED,
|
||||
Message::Surface,
|
||||
crate::Message::from,
|
||||
|
|
@ -282,21 +314,6 @@ fn input() -> Section<crate::pages::Message> {
|
|||
))
|
||||
.add(settings::item(&*section.descriptions[device], devices));
|
||||
|
||||
if !page.model.source_profiles().is_empty() {
|
||||
let dropdown = widget::dropdown::popup_dropdown(
|
||||
page.model.source_profiles(),
|
||||
page.model.active_source_profile(),
|
||||
Message::SourceProfileChanged,
|
||||
window::Id::RESERVED,
|
||||
Message::Surface,
|
||||
crate::Message::from,
|
||||
)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::from);
|
||||
|
||||
controls = controls.add(settings::item(&*section.descriptions[profile], dropdown));
|
||||
}
|
||||
|
||||
controls = controls.add(
|
||||
settings::item::builder(&*section.descriptions[amplification])
|
||||
.description(&*section.descriptions[amplification_desc])
|
||||
|
|
@ -316,7 +333,6 @@ fn output() -> Section<crate::pages::Message> {
|
|||
let volume = descriptions.insert(fl!("sound-output", "volume"));
|
||||
let device = descriptions.insert(fl!("sound-output", "device"));
|
||||
let _level = descriptions.insert(fl!("sound-output", "level"));
|
||||
let profile = descriptions.insert(fl!("profile"));
|
||||
let balance = descriptions.insert(fl!("sound-output", "balance"));
|
||||
let left = descriptions.insert(fl!("sound-output", "left"));
|
||||
let right = descriptions.insert(fl!("sound-output", "right"));
|
||||
|
|
@ -330,12 +346,12 @@ fn output() -> Section<crate::pages::Message> {
|
|||
.view::<Page>(move |_binder, page, section| {
|
||||
let slider = if page.amplification_sink {
|
||||
widget::slider(0..=150, page.model.sink_volume, |change| {
|
||||
Message::SinkVolumeChanged(change).into()
|
||||
Message::SetSinkVolume(change).into()
|
||||
})
|
||||
.breakpoints(&[100])
|
||||
} else {
|
||||
widget::slider(0..=100, page.model.sink_volume, |change| {
|
||||
Message::SinkVolumeChanged(change).into()
|
||||
Message::SetSinkVolume(change).into()
|
||||
})
|
||||
};
|
||||
|
||||
|
|
@ -347,7 +363,7 @@ fn output() -> Section<crate::pages::Message> {
|
|||
} else {
|
||||
widget::icon::from_name("audio-volume-high-symbolic")
|
||||
})
|
||||
.on_press(Message::SinkMuteToggle.into()),
|
||||
.on_press(Message::ToggleSinkMute.into()),
|
||||
)
|
||||
.push(
|
||||
widget::text::body(&page.model.sink_volume_text)
|
||||
|
|
@ -360,7 +376,7 @@ fn output() -> Section<crate::pages::Message> {
|
|||
let devices = widget::dropdown::popup_dropdown(
|
||||
page.model.sinks(),
|
||||
Some(page.model.active_sink().unwrap_or(0)),
|
||||
Message::SinkChanged,
|
||||
Message::SetDefaultSink,
|
||||
window::Id::RESERVED,
|
||||
Message::Surface,
|
||||
crate::Message::from,
|
||||
|
|
@ -374,24 +390,8 @@ fn output() -> Section<crate::pages::Message> {
|
|||
&*section.descriptions[volume],
|
||||
volume_control,
|
||||
))
|
||||
.add(settings::item(&*section.descriptions[device], devices));
|
||||
|
||||
if !page.model.sink_profiles().is_empty() {
|
||||
let dropdown = widget::dropdown::popup_dropdown(
|
||||
page.model.sink_profiles(),
|
||||
page.model.active_sink_profile(),
|
||||
Message::SinkProfileChanged,
|
||||
window::Id::RESERVED,
|
||||
Message::Surface,
|
||||
crate::Message::from,
|
||||
)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::from);
|
||||
|
||||
controls = controls.add(settings::item(&*section.descriptions[profile], dropdown));
|
||||
}
|
||||
if let Some(sink_balance) = page.model.sink_balance {
|
||||
controls = controls.add(settings::item(
|
||||
.add(settings::item(&*section.descriptions[device], devices))
|
||||
.add(settings::item(
|
||||
&*section.descriptions[balance],
|
||||
widget::row::with_capacity(4)
|
||||
.align_y(Alignment::Center)
|
||||
|
|
@ -404,8 +404,9 @@ fn output() -> Section<crate::pages::Message> {
|
|||
.push(
|
||||
widget::slider(
|
||||
0..=200,
|
||||
((sink_balance + 1.).max(0.) * 100.).round() as u32,
|
||||
|change| Message::SinkBalanceChanged(change).into(),
|
||||
(page.model.sink_balance.unwrap_or(1.0).max(0.) * 100.).round()
|
||||
as u32,
|
||||
|change| Message::SetSinkBalance(change).into(),
|
||||
)
|
||||
.breakpoints(&[100]),
|
||||
)
|
||||
|
|
@ -416,7 +417,6 @@ fn output() -> Section<crate::pages::Message> {
|
|||
.align_x(Alignment::Center),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
controls = controls.add(
|
||||
settings::item::builder(&*section.descriptions[amplification])
|
||||
|
|
@ -431,6 +431,34 @@ fn output() -> Section<crate::pages::Message> {
|
|||
})
|
||||
}
|
||||
|
||||
/// A section for opening the device profiles sub-page.
|
||||
fn device_profiles() -> Section<crate::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
button_txt = fl!("sound-device-profiles");
|
||||
});
|
||||
|
||||
Section::default()
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
let button = widget::row::with_children(vec![
|
||||
widget::horizontal_space().into(),
|
||||
widget::icon::from_name("go-next-symbolic").size(16).into(),
|
||||
]);
|
||||
|
||||
let device_profiles = settings::item::builder(&*descriptions[button_txt])
|
||||
.control(button)
|
||||
.spacing(16)
|
||||
.apply(widget::container)
|
||||
.class(cosmic::theme::Container::List)
|
||||
.apply(widget::button::custom)
|
||||
.class(cosmic::theme::Button::Transparent)
|
||||
.on_press(crate::pages::Message::Page(page.device_profiles));
|
||||
|
||||
settings::section().add(device_profiles).into()
|
||||
})
|
||||
}
|
||||
|
||||
// fn alerts() -> Section<crate::pages::Message> {
|
||||
// let mut descriptions = Slab::new();
|
||||
// let volume = descriptions.insert(fl!("sound-alerts", "volume"));
|
||||
16
crates/cosmic-pipewire/Cargo.toml
Normal file
16
crates/cosmic-pipewire/Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "cosmic-pipewire"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
rust-version.workspace = true
|
||||
license = "MPL-2.0"
|
||||
publish = true
|
||||
|
||||
[dependencies]
|
||||
intmap = "3.1.2"
|
||||
libspa = "0.9.2"
|
||||
libspa-sys = "0.9.2"
|
||||
pipewire = "0.9"
|
||||
serde = { version = "1.0.228", features = ["derive"]}
|
||||
serde_json = "1.0.145"
|
||||
tracing = "0.1.41"
|
||||
358
crates/cosmic-pipewire/LICENSE.md
Normal file
358
crates/cosmic-pipewire/LICENSE.md
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
## 1. Definitions
|
||||
|
||||
### 1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
### 1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
### 1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
### 1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
### 1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
+ (a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
+ (b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
### 1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
### 1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
### 1.8. "License"
|
||||
means this document.
|
||||
|
||||
### 1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
### 1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
+ (a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
+ (b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
### 1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
### 1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
### 1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
### 1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
## 2. License Grants and Conditions
|
||||
|
||||
### 2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
+ (a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
+ (b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
### 2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
### 2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
+ (a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
+ (b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
+ (c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
### 2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
### 2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
### 2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
### 2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
## 3. Responsibilities
|
||||
|
||||
### 3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
### 3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
+ (a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
+ (b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
### 3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
### 3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
### 3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
## 4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
## 5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
|
||||
## 6. Disclaimer of Warranty
|
||||
|
||||
**Covered Software is provided under this License on an "as is"
|
||||
basis, without warranty of any kind, either expressed, implied, or
|
||||
statutory, including, without limitation, warranties that the
|
||||
Covered Software is free of defects, merchantable, fit for a
|
||||
particular purpose or non-infringing. The entire risk as to the
|
||||
quality and performance of the Covered Software is with You.
|
||||
Should any Covered Software prove defective in any respect, You
|
||||
(not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an
|
||||
essential part of this License. No use of any Covered Software is
|
||||
authorized under this License except under this disclaimer.**
|
||||
|
||||
|
||||
#7. Limitation of Liability
|
||||
|
||||
**Under no circumstances and under no legal theory, whether tort
|
||||
(including negligence), contract, or otherwise, shall any
|
||||
Contributor, or anyone who distributes Covered Software as
|
||||
permitted above, be liable to You for any direct, indirect,
|
||||
special, incidental, or consequential damages of any character
|
||||
including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any
|
||||
and all other commercial damages or losses, even if such party
|
||||
shall have been informed of the possibility of such damages. This
|
||||
limitation of liability shall not apply to liability for death or
|
||||
personal injury resulting from such party's negligence to the
|
||||
extent applicable law prohibits such limitation. Some
|
||||
jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and
|
||||
limitation may not apply to You.**
|
||||
|
||||
|
||||
## 8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
## 9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
## 10. Versions of the License
|
||||
|
||||
### 10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
### 10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
### 10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
### 10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
## Exhibit A - Source Code Form License Notice
|
||||
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
## Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
27
crates/cosmic-pipewire/src/device.rs
Normal file
27
crates/cosmic-pipewire/src/device.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use pipewire::device::DeviceInfoRef;
|
||||
|
||||
/// Device information
|
||||
#[must_use]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Device {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Attains process info from a pipewire info node.
|
||||
#[must_use]
|
||||
pub fn from_device(info: &DeviceInfoRef) -> Option<Self> {
|
||||
let props = info.props()?;
|
||||
|
||||
let device = Device {
|
||||
id: props.get("object.id")?.parse::<u32>().ok()?,
|
||||
name: props.get("device.description")?.to_owned(),
|
||||
};
|
||||
|
||||
Some(device)
|
||||
}
|
||||
}
|
||||
1011
crates/cosmic-pipewire/src/lib.rs
Normal file
1011
crates/cosmic-pipewire/src/lib.rs
Normal file
File diff suppressed because it is too large
Load diff
190
crates/cosmic-pipewire/src/node.rs
Normal file
190
crates/cosmic-pipewire/src/node.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use crate::{Channel, spa_utils::array_from_pod};
|
||||
use libspa::{pod::Pod, utils::Id};
|
||||
use pipewire::node::{NodeInfoRef, NodeState};
|
||||
use std::ffi::c_float;
|
||||
|
||||
/// Node information
|
||||
#[must_use]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Node {
|
||||
pub object_id: u32,
|
||||
pub audio_channels: u32,
|
||||
pub audio_position: String,
|
||||
pub card_profile_device: Option<u32>,
|
||||
pub description: String,
|
||||
pub device_id: Option<u32>,
|
||||
pub device_profile_description: String,
|
||||
pub device_profile_pro: bool,
|
||||
pub icon_name: String,
|
||||
pub media_class: MediaClass,
|
||||
pub node_name: String,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum State {
|
||||
Idle,
|
||||
Running,
|
||||
Creating,
|
||||
Suspended,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum MediaClass {
|
||||
Source,
|
||||
Sink,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Attains process info from a pipewire info node.
|
||||
#[must_use]
|
||||
pub fn from_node(info: &NodeInfoRef) -> Option<Self> {
|
||||
let props = info.props()?;
|
||||
|
||||
let mut audio_channels = 1;
|
||||
let mut audio_position = String::new();
|
||||
let mut card_profile_device = None;
|
||||
let mut device_id = None;
|
||||
let mut device_profile_description: &str = "";
|
||||
let mut device_profile_pro = false;
|
||||
let mut icon_name = String::new();
|
||||
let mut media_class = None;
|
||||
let mut node_description: &str = "";
|
||||
let mut node_name = String::new();
|
||||
let mut object_id = None;
|
||||
|
||||
for (entry, value) in props.iter() {
|
||||
match entry {
|
||||
"device.id" => device_id = value.parse::<u32>().ok(),
|
||||
"object.id" => object_id = Some(value.parse::<u32>().ok()?),
|
||||
|
||||
// 2
|
||||
"audio.channels" => audio_channels = value.parse::<u32>().unwrap_or(1),
|
||||
|
||||
// FL,FR
|
||||
"audio.position" => audio_position = value.to_owned(),
|
||||
|
||||
// 0
|
||||
"card.profile.device" => card_profile_device = Some(value.parse::<u32>().ok()?),
|
||||
|
||||
// Analog Stereo (ALSA only)
|
||||
"device.profile.description" => {
|
||||
device_profile_description = value;
|
||||
}
|
||||
|
||||
// false
|
||||
"device.profile.pro" => device_profile_pro = value == "true",
|
||||
|
||||
// audio-card-analog
|
||||
"device.icon-name" => icon_name = value.to_owned(),
|
||||
|
||||
"media.class" => {
|
||||
media_class = Some(match value {
|
||||
"Audio/Sink" => MediaClass::Sink,
|
||||
"Audio/Source" => MediaClass::Source,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
// alsa_input.pci-0000_66_00.6.analog-stereo
|
||||
"node.name" => node_name = value.to_owned(),
|
||||
|
||||
// Family 17h/19h HD Audio Controller Analog Stereo
|
||||
"node.description" => node_description = value,
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let device = Node {
|
||||
object_id: object_id?,
|
||||
device_id,
|
||||
card_profile_device,
|
||||
media_class: media_class?,
|
||||
description: if device_profile_description.is_empty() {
|
||||
node_description.to_owned()
|
||||
} else {
|
||||
let device_name = node_description
|
||||
.strip_suffix(device_profile_description)
|
||||
.unwrap_or(node_description)
|
||||
.trim_ascii_end();
|
||||
device_name.to_owned()
|
||||
},
|
||||
device_profile_description: device_profile_description.to_owned(),
|
||||
device_profile_pro,
|
||||
icon_name,
|
||||
audio_channels,
|
||||
audio_position,
|
||||
node_name,
|
||||
state: match info.state() {
|
||||
NodeState::Idle => State::Idle,
|
||||
NodeState::Running => State::Running,
|
||||
NodeState::Creating => State::Creating,
|
||||
NodeState::Suspended => State::Suspended,
|
||||
NodeState::Error(why) => State::Error(why.to_owned()),
|
||||
},
|
||||
};
|
||||
|
||||
Some(device)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NodeProps {
|
||||
pub mute: Option<bool>,
|
||||
pub monitor_mute: Option<bool>,
|
||||
pub channel_map: Option<Vec<Channel>>,
|
||||
pub channel_volumes: Option<Vec<f32>>,
|
||||
}
|
||||
|
||||
impl NodeProps {
|
||||
pub fn from_pod(pod: &Pod) -> Option<Self> {
|
||||
let props = pod.as_object().ok()?;
|
||||
let props = NodeProps {
|
||||
mute: props
|
||||
.find_prop(Id(libspa_sys::SPA_PROP_mute))
|
||||
.and_then(|prop| prop.value().get_bool().ok()),
|
||||
monitor_mute: props
|
||||
.find_prop(Id(libspa_sys::SPA_PROP_monitorMute))
|
||||
.and_then(|prop| prop.value().get_bool().ok()),
|
||||
channel_map: props
|
||||
.find_prop(Id(libspa_sys::SPA_PROP_channelMap))
|
||||
.and_then(|prop| unsafe { array_from_pod::<Channel>(prop.value()) }),
|
||||
channel_volumes: props
|
||||
.find_prop(Id(libspa_sys::SPA_PROP_channelVolumes))
|
||||
.and_then(|prop| unsafe { array_from_pod::<c_float>(prop.value()) }),
|
||||
};
|
||||
|
||||
if props.mute.is_none()
|
||||
&& props.monitor_mute.is_none()
|
||||
&& props.channel_map.is_none()
|
||||
&& props.channel_volumes.is_none()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(props)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: NodeProps) {
|
||||
if other.mute.is_some() {
|
||||
self.mute = other.mute
|
||||
}
|
||||
|
||||
if other.monitor_mute.is_some() {
|
||||
self.monitor_mute = other.monitor_mute;
|
||||
}
|
||||
|
||||
if other.channel_map.is_some() {
|
||||
self.channel_map = other.channel_map;
|
||||
}
|
||||
|
||||
if other.channel_volumes.is_some() {
|
||||
self.channel_volumes = other.channel_volumes;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
crates/cosmic-pipewire/src/port.rs
Normal file
98
crates/cosmic-pipewire/src/port.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! Currently unusued
|
||||
|
||||
use crate::pipewire::Direction;
|
||||
use pipewire::port::PortInfoRef;
|
||||
|
||||
#[must_use]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Port {
|
||||
pub node_id: u32,
|
||||
pub object_id: u32,
|
||||
pub port_id: u32,
|
||||
pub audio_channel: String,
|
||||
pub format_dsp: String,
|
||||
pub object_path: String,
|
||||
pub port_direction: Direction,
|
||||
pub port_group: String,
|
||||
pub port_name: String,
|
||||
pub port_alias: String,
|
||||
pub port_physical: bool,
|
||||
pub port_terminal: bool,
|
||||
pub port_monitor: bool,
|
||||
}
|
||||
|
||||
impl Port {
|
||||
/// Attains process info from a pipewire info port.
|
||||
#[must_use]
|
||||
pub fn from_port(info: &PortInfoRef) -> Option<Self> {
|
||||
let props = info.props()?;
|
||||
let object_id = info.id();
|
||||
let port_direction = match info.direction() {
|
||||
libspa::utils::Direction::Input => Direction::Input,
|
||||
libspa::utils::Direction::Output => Direction::Output,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let mut node_id = 0;
|
||||
let mut port_id = 0;
|
||||
let mut port_monitor = false;
|
||||
let mut port_physical = false;
|
||||
let mut port_terminal = false;
|
||||
|
||||
let mut audio_channel = String::new();
|
||||
let mut format_dsp = String::new();
|
||||
let mut object_path = String::new();
|
||||
let mut port_alias = String::new();
|
||||
let mut port_group = String::new();
|
||||
let mut port_name = String::new();
|
||||
|
||||
for (entry, value) in props.iter() {
|
||||
match entry {
|
||||
// 32 bit float mono audio
|
||||
"format.dsp" => format_dsp = value.to_owned(),
|
||||
// FR
|
||||
"audio.channel" => audio_channel = value.to_owned(),
|
||||
// playback
|
||||
"port.group" => port_group = value.to_owned(),
|
||||
// 1
|
||||
"port.id" => port_id = value.parse::<u32>().ok()?,
|
||||
// false
|
||||
"port.monitor" => port_monitor = value == "true",
|
||||
// true
|
||||
"port.physical" => port_physical = value == "true",
|
||||
// true
|
||||
"port.terminal" => port_terminal = value == "true",
|
||||
// alsa:acp:Device:3:playback:playback_1
|
||||
"object.path" => object_path = value.to_owned(),
|
||||
// playback_FR
|
||||
"port.name" => port_name = value.to_owned(),
|
||||
// MosArt USB Audio Device:playback_FR
|
||||
"port.alias" => port_alias = value.to_owned(),
|
||||
// 59
|
||||
"node.id" => node_id = value.parse::<u32>().ok()?,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let port = Port {
|
||||
format_dsp,
|
||||
audio_channel,
|
||||
port_id,
|
||||
port_direction,
|
||||
object_path,
|
||||
port_name,
|
||||
port_alias,
|
||||
port_group,
|
||||
port_monitor,
|
||||
port_physical,
|
||||
port_terminal,
|
||||
node_id,
|
||||
object_id,
|
||||
};
|
||||
|
||||
Some(port)
|
||||
}
|
||||
}
|
||||
53
crates/cosmic-pipewire/src/profile.rs
Normal file
53
crates/cosmic-pipewire/src/profile.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use crate::{Availability, spa_utils::string_from_pod};
|
||||
use libspa::pod::Pod;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Profile {
|
||||
pub index: i32,
|
||||
pub priority: i32,
|
||||
pub available: Availability,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn from_pod(pod: &Pod) -> Option<Self> {
|
||||
let mut index = 0;
|
||||
let mut priority = 0;
|
||||
let mut available = Availability::Unknown;
|
||||
let mut name = String::new();
|
||||
let mut description = String::new();
|
||||
|
||||
let profile = pod.as_object().ok()?;
|
||||
|
||||
for prop in profile.props() {
|
||||
match prop.key().0 {
|
||||
libspa_sys::SPA_PARAM_PROFILE_index => index = prop.value().get_int().ok()?,
|
||||
libspa_sys::SPA_PARAM_PROFILE_priority => priority = prop.value().get_int().ok()?,
|
||||
libspa_sys::SPA_PARAM_PROFILE_available => {
|
||||
available = match prop.value().get_id().unwrap().0 {
|
||||
libspa_sys::SPA_PARAM_AVAILABILITY_no => Availability::No,
|
||||
libspa_sys::SPA_PARAM_AVAILABILITY_yes => Availability::Yes,
|
||||
_ => Availability::Unknown,
|
||||
};
|
||||
}
|
||||
libspa_sys::SPA_PARAM_PROFILE_name => name = string_from_pod(prop.value())?,
|
||||
libspa_sys::SPA_PARAM_PROFILE_description => {
|
||||
description = string_from_pod(prop.value())?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
index,
|
||||
priority,
|
||||
available,
|
||||
name,
|
||||
description,
|
||||
})
|
||||
}
|
||||
}
|
||||
92
crates/cosmic-pipewire/src/route.rs
Normal file
92
crates/cosmic-pipewire/src/route.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::ffi::{c_float, c_int};
|
||||
|
||||
use crate::{
|
||||
Availability, Channel, Direction,
|
||||
spa_utils::{array_from_pod, string_from_pod},
|
||||
};
|
||||
use libspa::{pod::Pod, utils::Id};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Route {
|
||||
pub index: i32,
|
||||
pub priority: i32,
|
||||
pub device: i32,
|
||||
pub available: Availability,
|
||||
pub direction: Direction,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub devices: Vec<i32>,
|
||||
pub props: Option<RouteProps>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RouteProps {
|
||||
pub mute: Option<bool>,
|
||||
pub monitor_mute: Option<bool>,
|
||||
pub channel_map: Option<Vec<Channel>>,
|
||||
pub channel_volumes: Option<Vec<f32>>,
|
||||
}
|
||||
|
||||
impl Route {
|
||||
pub fn from_pod(pod: &Pod) -> Option<Self> {
|
||||
let mut this = Route::default();
|
||||
|
||||
let route = pod.as_object().ok()?;
|
||||
|
||||
for prop in route.props() {
|
||||
match prop.key().0 {
|
||||
libspa_sys::SPA_PARAM_ROUTE_index => this.index = prop.value().get_int().ok()?,
|
||||
libspa_sys::SPA_PARAM_ROUTE_priority => {
|
||||
this.priority = prop.value().get_int().ok()?
|
||||
}
|
||||
libspa_sys::SPA_PARAM_ROUTE_device => this.device = prop.value().get_int().ok()?,
|
||||
libspa_sys::SPA_PARAM_ROUTE_available => {
|
||||
this.available = match prop.value().get_id().unwrap().0 {
|
||||
libspa_sys::SPA_PARAM_AVAILABILITY_no => Availability::No,
|
||||
libspa_sys::SPA_PARAM_AVAILABILITY_yes => Availability::Yes,
|
||||
_ => Availability::Unknown,
|
||||
};
|
||||
}
|
||||
libspa_sys::SPA_PARAM_ROUTE_name => this.name = string_from_pod(prop.value())?,
|
||||
libspa_sys::SPA_PARAM_ROUTE_description => {
|
||||
this.description = string_from_pod(prop.value())?;
|
||||
}
|
||||
libspa_sys::SPA_PARAM_ROUTE_direction => {
|
||||
this.direction = match prop.value().get_id().unwrap().0 {
|
||||
libspa_sys::SPA_DIRECTION_OUTPUT => Direction::Output,
|
||||
_ => Direction::Input,
|
||||
}
|
||||
}
|
||||
libspa_sys::SPA_PARAM_ROUTE_devices => {
|
||||
if let Some(data) = unsafe { array_from_pod::<c_int>(prop.value()) } {
|
||||
this.devices = data;
|
||||
}
|
||||
}
|
||||
libspa_sys::SPA_PARAM_ROUTE_props => {
|
||||
let props = prop.value().as_object().ok()?;
|
||||
|
||||
this.props = Some(RouteProps {
|
||||
mute: props
|
||||
.find_prop(Id(libspa_sys::SPA_PROP_mute))
|
||||
.and_then(|prop| prop.value().get_bool().ok()),
|
||||
monitor_mute: props
|
||||
.find_prop(Id(libspa_sys::SPA_PROP_monitorMute))
|
||||
.and_then(|prop| prop.value().get_bool().ok()),
|
||||
channel_map: props
|
||||
.find_prop(Id(libspa_sys::SPA_PROP_channelMap))
|
||||
.and_then(|prop| unsafe { array_from_pod::<Channel>(prop.value()) }),
|
||||
channel_volumes: props
|
||||
.find_prop(Id(libspa_sys::SPA_PROP_channelVolumes))
|
||||
.and_then(|prop| unsafe { array_from_pod::<c_float>(prop.value()) }),
|
||||
})
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Some(this)
|
||||
}
|
||||
}
|
||||
152
crates/cosmic-pipewire/src/spa_utils.rs
Normal file
152
crates/cosmic-pipewire/src/spa_utils.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use libspa::pod::Pod;
|
||||
use std::ffi::CStr;
|
||||
|
||||
/// Read a `Pod`'s string if it contains a string.
|
||||
pub fn string_from_pod(pod: &Pod) -> Option<String> {
|
||||
if !pod.is_string() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut cstr = std::ptr::null();
|
||||
|
||||
unsafe {
|
||||
// SAFETY: Pod is checked to be a string beforehand
|
||||
if libspa_sys::spa_pod_get_string(pod.as_raw_ptr(), &mut cstr) == 0 {
|
||||
if !cstr.is_null() {
|
||||
return Some(String::from_utf8_lossy(CStr::from_ptr(cstr).to_bytes()).into_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// SAFETY: Must be absolutely certain that the array is a compatible array.
|
||||
pub unsafe fn array_from_pod<CType: Copy>(pod: &Pod) -> Option<Vec<CType>> {
|
||||
if !pod.is_array() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut len = 0;
|
||||
|
||||
unsafe {
|
||||
let array: *mut CType = libspa_sys::spa_pod_get_array(pod.as_raw_ptr(), &mut len).cast();
|
||||
|
||||
if array.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(std::slice::from_raw_parts(array, len as usize).to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Copy, Clone, Debug, Default, Hash, Eq, PartialEq)]
|
||||
pub enum Channel {
|
||||
#[default]
|
||||
UNKNOWN = 0, // unspecified
|
||||
NA, // N/A, silent
|
||||
MONO, // mono stream
|
||||
FL, // front left
|
||||
FR, // front right
|
||||
FC, // front center
|
||||
LFE, // LFE
|
||||
SL, // side left
|
||||
SR, // side right
|
||||
FLC, // front left center
|
||||
FRC, // front right center
|
||||
RC, // rear center
|
||||
RL, // rear left
|
||||
RR, // rear right
|
||||
TC, // top center
|
||||
TFL, // top front left
|
||||
TFC, // top front center
|
||||
TFR, // top front right
|
||||
TRL, // top rear left
|
||||
TRC, // top rear center
|
||||
TRR, // top rear right
|
||||
RLC, // rear left center
|
||||
RRC, // rear right center
|
||||
FLW, // front left wide
|
||||
FRW, // front right wide
|
||||
LFE2, // LFE 2
|
||||
FLH, // front left high
|
||||
FCH, // front center high
|
||||
FRH, // front right high
|
||||
TFLC, // top front left center
|
||||
TFRC, // top front right center
|
||||
TSL, // top side left
|
||||
TSR, // top side right
|
||||
LLFE, // left LFE
|
||||
RLFE, // right LFE
|
||||
BC, // bottom center
|
||||
BLC, // bottom left center
|
||||
BRC = 37, // bottom right center
|
||||
AUX0 = 4096, // aux channels
|
||||
AUX1,
|
||||
AUX2,
|
||||
AUX3,
|
||||
AUX4,
|
||||
AUX5,
|
||||
AUX6,
|
||||
AUX7,
|
||||
AUX8,
|
||||
AUX9,
|
||||
AUX10,
|
||||
AUX11,
|
||||
AUX12,
|
||||
AUX13,
|
||||
AUX14,
|
||||
AUX15,
|
||||
AUX16,
|
||||
AUX17,
|
||||
AUX18,
|
||||
AUX19,
|
||||
AUX20,
|
||||
AUX21,
|
||||
AUX22,
|
||||
AUX23,
|
||||
AUX24,
|
||||
AUX25,
|
||||
AUX26,
|
||||
AUX27,
|
||||
AUX28,
|
||||
AUX29,
|
||||
AUX30,
|
||||
AUX31,
|
||||
AUX32,
|
||||
AUX33,
|
||||
AUX34,
|
||||
AUX35,
|
||||
AUX36,
|
||||
AUX37,
|
||||
AUX38,
|
||||
AUX39,
|
||||
AUX40,
|
||||
AUX41,
|
||||
AUX42,
|
||||
AUX43,
|
||||
AUX44,
|
||||
AUX45,
|
||||
AUX46,
|
||||
AUX47,
|
||||
AUX48,
|
||||
AUX49,
|
||||
AUX50,
|
||||
AUX51,
|
||||
AUX52,
|
||||
AUX53,
|
||||
AUX54,
|
||||
AUX55,
|
||||
AUX56,
|
||||
AUX57,
|
||||
AUX58,
|
||||
AUX59,
|
||||
AUX60,
|
||||
AUX61,
|
||||
AUX62,
|
||||
AUX63 = 4159,
|
||||
}
|
||||
1
debian/control
vendored
1
debian/control
vendored
|
|
@ -13,7 +13,6 @@ Build-Depends:
|
|||
libfreetype-dev,
|
||||
libinput-dev,
|
||||
libpipewire-0.3-dev,
|
||||
libpulse-dev,
|
||||
libudev-dev,
|
||||
libwayland-dev,
|
||||
libxkbcommon-dev,
|
||||
|
|
|
|||
9
i18n/en/cosmic_settings.ftl
vendored
9
i18n/en/cosmic_settings.ftl
vendored
|
|
@ -487,10 +487,15 @@ sound-alerts = Alerts
|
|||
sound-applications = Applications
|
||||
.desc = Application volumes and settings
|
||||
|
||||
profile = Profile
|
||||
# No speaker, headphones, or microphone plugged into sound card port
|
||||
sound-device-port-unplugged = Unplugged
|
||||
sound-hd-audio = HD Audio
|
||||
sound-usb-audio = USB Audio
|
||||
|
||||
## Power
|
||||
# Profiles for sound card devices
|
||||
sound-device-profiles = Device profiles
|
||||
|
||||
# Power & Battery settings page
|
||||
power = Power & battery
|
||||
.desc = Manage power settings
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings-page"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings-system"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-only"
|
||||
rust-version.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings-wallpaper"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
rust-version.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Comment[af]=Toestelnaam, hardeware-inligtings, standaardinstellings van die bedr
|
|||
Comment[sk]=Názov zariadenia, hardvérové informácie, predvolené nastavenia systému.
|
||||
Comment[sv]=Enhetsnamn, hårdvaruinformation, standardinställningar för operativsystem.
|
||||
Comment[es]=Nombre del dispositivo, información de hardware y valores del sistema operativo.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings about
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[af]=Toeganklikheidsinstellings.
|
|||
Comment[sk]=Nastavenia prístupnosti.
|
||||
Comment[sv]=Tillgänglighetsinställningar.
|
||||
Comment[es]=Configuración de accesibilidad.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings accessibility
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Comment[af]=Aksentkleure en temas.
|
|||
Comment[sk]=Akcentové farby a témy.
|
||||
Comment[sv]=Accentfärger och teman.
|
||||
Comment[es]=Colores de énfasis y temas.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings appearance
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[af]=Bestuur toepassingsinstellings.
|
|||
Comment[sk]=Spravovať nastavenia aplikácií.
|
||||
Comment[sv]=Hantera programinställningar.
|
||||
Comment[es]=Gestionar configuración de aplicaciones.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings applications
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[sv]=Hantera Bluetooth-enheter
|
|||
Comment[nl]=Bluetooth-apparaten beheren
|
||||
Comment[af]=Bestuur Bluetooth-toestelle
|
||||
Comment[es]=Gestionar dispositivos Bluetooth
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings bluetooth
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Comment[af]=Tydsone, outomatiese klokinstellings en tydformatering.
|
|||
Comment[sk]=Časové pásmo, automatické nastavenie hodín a formátovanie času.
|
||||
Comment[sv]=Tidzon, automatiska klockinställningar, och tidsformat.
|
||||
Comment[es]=Zona horaria, configuración automática del reloj y formatos de hora.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings date-time
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Comment[af]=Standaard webblaaier, e-poskliënt, lêerblaaier en ander toepassing
|
|||
Comment[sk]=Predvolený webový prehliadač, e-mailový klient, správca súborov a ďalšie aplikácie.
|
||||
Comment[sv]=Standardwebläsare, e-postklient, filbläddrare, och andra program.
|
||||
Comment[es]=Navegador web predeterminado, cliente de correo, explorador de archivos y otras aplicaciones.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings default-apps
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Name[es]=Escritorio
|
|||
Comment=
|
||||
Comment[cs]=Nastavení pracovní plochy, vzhledu a chování oken.
|
||||
Comment[sk]=Nastavenia pracovnej plochy, vzhľadu a správania okien
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings desktop
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Comment[af]=Vertoonopsies, grafiese modusse en naglig.
|
|||
Comment[sk]=Možnosti displeja, grafické režimy a nočné svetlo.
|
||||
Comment[sv]=Skärmalternativ, grafiklägen, och nattljus.
|
||||
Comment[es]=Opciones de pantalla, modos gráficos y luz nocturna.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings displays
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[sv]=En valfri list för program och applets.
|
|||
Comment[nl]=Een optionele balk voor apps en applets.
|
||||
Comment[af]='n Opsionele balk vir programme en applets.
|
||||
Comment[es]=Panel opcional para aplicaciones y otros applets.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings dock
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[af]=Bekyk en werk firmware op.
|
|||
Comment[sk]=Zobraziť a aktualizovať firmware.
|
||||
Comment[sv]=Visa och updatera fast programvara.
|
||||
Comment[es]=Ver y actualizar firmware.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings firmware
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[hu]=Billentyűzet, mutató, stb.
|
|||
Comment[sk]=Klávesnica, kurzor a ďalšie.
|
||||
Comment[sv]=Tangentbord, markör, etc.
|
||||
Comment[es]=Teclado, ratón, etc.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings input
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[nl]=Invoermethodes, speciale tekens, en sneltoetsen.
|
|||
Comment[sk]=Vstupné zdroje, prepínanie, zadávanie špeciálnych znakov, skratky.
|
||||
Comment[sv]=Inmatningskällor, växling, specialtecken, genvägar.
|
||||
Comment[es]=Entrada de teclado, conmutación, carácteres especiales, atajos.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings keyboard
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Comment[nl]=X11-toepassingsvensters schalen, en globale sneltoetsen.
|
|||
Comment[sk]=Škálovanie X11 aplikácií a globálne skratky.
|
||||
Comment[sv]=Applikationsskalning för X11 fönstersystem och globala genvägar.
|
||||
Comment[es]=Escalado de aplicaciones del sistema de ventanas X11 y atajos globales.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings legacy-applications
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[nl]=Muissnelheid en -versnelling, en 'natuurlijk' scrollen.
|
|||
Comment[sk]=Rýchlosť myši, akcelerácia a prirodzené rolovanie.
|
||||
Comment[sv]=Mushastighet, acceleration, och naturlig rullning.
|
||||
Comment[es]=Velocidad del ratón, aceleración y desplazamiento natural.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings mouse
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[Desktop Entry]
|
||||
Name=Network & Wireless
|
||||
Name[ar]=الشبكة والاتصالات اللاسلكية
|
||||
Name[ar]=الشبكة والاتصالات اللاسلكية
|
||||
Name[cs]=Síť a Wi-Fi
|
||||
Name[zh_CN]=网络和无线
|
||||
Name[pl]=Sieć i połączenia bezprzewodowe
|
||||
|
|
@ -21,7 +21,7 @@ Comment[nl]=Netwerkverbindingen beheren
|
|||
Comment[sk]=Spravovať sieťové pripojenia
|
||||
Comment[sv]=Hantera nätverksanslutningar
|
||||
Comment[es]=Gestionar conexiones de red
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings network
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[nl]="Niet storen", meldingen op het vergrendelingsscherm en meldingsinst
|
|||
Comment[sk]=Nerušiť, oznámenia na uzamknutej obrazovke a nastavenia pre aplikácie.
|
||||
Comment[sv]=Stör ej, aviseringar på låsskärm, och inställningar per program.
|
||||
Comment[es]=No molestar, notificaciones de pantalla de bloqueo y ajustes de aplicación.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings notifications
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Comment[nl]=De standaard systeembalk voor menu's en applets.
|
|||
Comment[sk]=Hlavný systémový panel pre menu a applety.
|
||||
Comment[sv]=Primär systemfält för menyer och applets.
|
||||
Comment[es]=Barra principal del sistema para menús y applets.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings panel
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[nl]=Energieverbruik en -besparingsopties.
|
|||
Comment[sk]=Režimy napájania a možnosti úspory energie.
|
||||
Comment[sv]=Strömalternativ och energisparalternativ.
|
||||
Comment[es]=Modos de energía y opciones de ahorro de energía.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings power
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[Desktop Entry]
|
||||
Name=Region & Language
|
||||
Name[ar]=اللغة والمنطقة
|
||||
Name[ar]=اللغة والمنطقة
|
||||
Name[cs]=Region a jazyk
|
||||
Name[zh_CN]=区域和语言
|
||||
Name[pl]=Region i język
|
||||
|
|
@ -21,7 +21,7 @@ Comment[nl]=Regionale datum-, tijd- en getalweergave.
|
|||
Comment[sk]=Formátovanie dátumov, časov a čísel podľa vášho regiónu.
|
||||
Comment[sv]=Formatera datum, tider och siffror baserat på din region.
|
||||
Comment[es]=Formato de fechas, horas y números según su región.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings region-language
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[nl]=Geluidsinstellingen voor apparaten, alarmen en programma's.
|
|||
Comment[sk]=Zvukové nastavenia pre zariadenia, upozornenia a aplikácie.
|
||||
Comment[sv]=Ljudinställningar för enhter, larm och program.
|
||||
Comment[es]=Configuraciones de audio para dispositivos, alertas y aplicaciones.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings sound
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Comment[ar]=اضبط التطبيقات التي تعمل عند الولوج.
|
|||
Comment[cs]=Nastavte aplikace, které se spustí při přihlášení.
|
||||
Comment[sv]=Konfigurera program som körs vid inloggning.
|
||||
Comment[hu]=Azoknak az alkalmazásoknak a beállítása, amelyek bejelentkezéskor elindulnak.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings startup-apps
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Comment[cs]=Systémové informace, uživatelé a firmware
|
|||
Comment[nl]=Systeeminformatie, gebruikers en firmware
|
||||
Comment[sk]=Systémové informácie, používatelia a firmware
|
||||
Comment[es]=Información del sistema, cuentas y firmware
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings system
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Name[sv]=Tid & språk
|
|||
Name[es]=Hora e Idioma
|
||||
Comment=
|
||||
Comment[sk]=Nastavenia času a jazyka
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings time
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[Desktop Entry]
|
||||
Name=Touchpad
|
||||
Name[ar]=لوحة اللمس
|
||||
Name[ar]=لوحة اللمس
|
||||
Name[cs]=Touchpad
|
||||
Name[zh_CN]=触摸板
|
||||
Name[pl]=Gładzik
|
||||
|
|
@ -20,7 +20,7 @@ Comment[sk]=Rýchlosť touchpadu, možnosti kliknutia, gestá.
|
|||
Comment[sv]=Pekplattans hastighet, klickalternativ, gester.
|
||||
Comment[nl]=Touchpad muisversnelling, klikeigenschappen en veeggebaren.
|
||||
Comment[es]=Velocidad del panel táctil, opciones de clic, gestos.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings touchpad
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[nl]=Authenticatie en gebruikersinstellingen.
|
|||
Comment[sk]=Autentifikácia a používateľské účty.
|
||||
Comment[sv]=Autentisering och användarkonton.
|
||||
Comment[es]=Autenticación y cuentas de usuario.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings users
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Comment[hu]=VPN-kapcsolatok és kapcsolódási profilok.
|
|||
Comment[pt]=Conexões VPN e perfis de conexão.
|
||||
Comment[nl]=VPN-verbindingen en VPN-profielen.
|
||||
Comment[es]=Conexiones VPN y perfiles de conexión.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings vpn
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ Comment[zh_CN]=壁纸图片、颜色和幻灯片选项
|
|||
Comment[pl]=Obraz tła, kolory i opcje pokazu slajdów.
|
||||
Comment[hu]=Háttérképek, színek és diavetítési beállítások.
|
||||
Comment[pt]=Imagens de plano de fundo, cores, e opções de exibição em slide.
|
||||
Comment[nl]=Schermachtergrond: Afbeeldingen, kleuren en diavoorstellingen.
|
||||
Comment[nl]=Schermachtergrond: Afbeeldingen, kleuren en diavoorstellingen.
|
||||
Comment[sk]=Obrázky tapiet, farby a možnosti prezentácie.
|
||||
Comment[sv]=Bakgrundsbilder, färger, och bildspelsalternativ.
|
||||
Comment[es]=Imágenes de fondo, colores y opciones de carrusel de imágenes.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings wallpaper
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[nl]=Opties voor de Supertoets, vensterbeheer en aanvullende opties voor
|
|||
Comment[sk]=Akcia klávesu Super, možnosti ovládania okien a ďalšie možnosti dlaždicovania okien.
|
||||
Comment[sv]=Åtgärd för Super-tangent, fönsterkontroll alternativ, och ytterligare fönsterplacerings alternativ.
|
||||
Comment[es]=Acción de la tecla Super, opciones de control de ventana y opciones adicionales de mosaico de ventana.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings window-management
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[nl]=Kabelverbinding en verbindingsprofielen.
|
|||
Comment[sk]=Káblové pripojenia a profily pripojení.
|
||||
Comment[sv]=Trådbundna anslutningar och anslutningsprofiler.
|
||||
Comment[es]=Conexiones cableadas y perfiles de conexión.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings wired
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Comment[nl]=Wifiverbinding en verbindingsprofielen.
|
|||
Comment[sk]=Wi-Fi pripojenia a profily pripojení.
|
||||
Comment[sv]=Wi-Fi-anslutningar och anslutningsprofiler.
|
||||
Comment[es]=Conexiones Wi-Fi y perfiles de conexión.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings wireless
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Comment[pt]=Orientação e comportamento da área de trabalho.
|
|||
Comment[sk]=Orientácia a správanie pracovných priestorov.
|
||||
Comment[sv]=Arbetsytors orientering och beteende.
|
||||
Comment[es]=Orientación de los espacios de trabajo y comportamiento.
|
||||
Type=Settings
|
||||
Type=Application
|
||||
Exec=cosmic-settings workspaces
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings-a11y-manager-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings-accessibility-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings-airplane-mode-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings-bluetooth-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings-network-manager-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings-daemon-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
rust-version.workspace = true
|
||||
publish = true
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
[package]
|
||||
name = "cosmic-settings-sound-subscription"
|
||||
version = "1.0.0-beta1"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
rust-version.workspace = true
|
||||
license = "MPL-2.0"
|
||||
publish = true
|
||||
|
||||
[dependencies]
|
||||
async-fn-stream = "0.3.2"
|
||||
cosmic-pipewire = { path = "../../crates/cosmic-pipewire" }
|
||||
crossbeam-queue = "0.3.12"
|
||||
futures = "0.3.31"
|
||||
indexmap = "2.12.0"
|
||||
intmap = "3.1.2"
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic" }
|
||||
libpulse-binding = "2.30.1"
|
||||
log = "0.4.28"
|
||||
pipewire = "0.8"
|
||||
rustix = "1.1.2"
|
||||
tokio = "1.48.0"
|
||||
numtoa = "1.0.0-alpha1"
|
||||
rustix = "1.0.8"
|
||||
tokio = { version = "1.47.1", features = ["process", "rt", "time"] }
|
||||
tracing = "0.1.41"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,279 +0,0 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// #![deny(missing_docs)]
|
||||
|
||||
pub use pipewire::channel::Sender;
|
||||
|
||||
use cosmic::iced_futures::{self, Subscription, stream};
|
||||
use futures::{SinkExt, executor::block_on};
|
||||
use pipewire::{
|
||||
context::Context as PwContext,
|
||||
main_loop::MainLoop as PwMainLoop,
|
||||
node::{Node, NodeInfoRef, NodeState},
|
||||
proxy::{Listener, ProxyT},
|
||||
types::ObjectType,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{BTreeMap, HashMap},
|
||||
rc::Rc,
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
pub fn subscription() -> iced_futures::Subscription<DeviceEvent> {
|
||||
Subscription::run_with_id(
|
||||
"pipewire",
|
||||
stream::channel(20, |sender| async {
|
||||
_ = thread(sender);
|
||||
|
||||
futures::future::pending().await
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn thread(
|
||||
on_event: futures::channel::mpsc::Sender<DeviceEvent>,
|
||||
) -> (JoinHandle<()>, pipewire::channel::Sender<()>) {
|
||||
let (pw_tx, pw_rx) = pipewire::channel::channel();
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
devices_from_socket(pw_rx, on_event);
|
||||
});
|
||||
|
||||
(handle, pw_tx)
|
||||
}
|
||||
|
||||
/// Node event`
|
||||
#[derive(Debug)]
|
||||
pub enum NodeEvent<'a> {
|
||||
/// Node info
|
||||
NodeInfo(u32, &'a NodeInfoRef),
|
||||
/// Node removal
|
||||
Remove(u32),
|
||||
}
|
||||
|
||||
/// Device event
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DeviceEvent {
|
||||
/// A new device was detected.
|
||||
Add(Device),
|
||||
/// A device with the given object_id was removed.
|
||||
Remove(u32),
|
||||
}
|
||||
|
||||
/// Device information
|
||||
#[must_use]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Device {
|
||||
pub object_id: u32,
|
||||
pub variant: DeviceVariant,
|
||||
pub media_class: MediaClass,
|
||||
pub product_name: String,
|
||||
pub node_name: String,
|
||||
pub state: DeviceState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum DeviceVariant {
|
||||
Alsa { alsa_card: u32 },
|
||||
Bluez5 { address: String },
|
||||
Unknown {},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DeviceState {
|
||||
Idle,
|
||||
Running,
|
||||
Creating,
|
||||
Suspended,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum MediaClass {
|
||||
Source,
|
||||
Sink,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Attains process info from a pipewire info node.
|
||||
#[must_use]
|
||||
pub fn from_node(info: &NodeInfoRef) -> Option<Self> {
|
||||
let props = info.props()?;
|
||||
|
||||
let (variant, product_name) = if let Some(alsa_card) =
|
||||
props.get("alsa.card").and_then(|v| v.parse::<u32>().ok())
|
||||
{
|
||||
let device_profile_description = props.get("device.profile.description")?.to_owned();
|
||||
|
||||
let description = props.get("node.description")?;
|
||||
|
||||
let description = description
|
||||
.strip_suffix(&device_profile_description)
|
||||
.map(str::trim_end)
|
||||
.unwrap_or(description)
|
||||
.replace("High Definition Audio", "HD Audio");
|
||||
|
||||
(DeviceVariant::Alsa { alsa_card }, description)
|
||||
} else if let Some(address) = props
|
||||
.get("api.bluez5.address")
|
||||
.and_then(|v| v.parse::<String>().ok())
|
||||
{
|
||||
(
|
||||
DeviceVariant::Bluez5 {
|
||||
address: address.to_owned(),
|
||||
},
|
||||
props.get("node.description")?.to_owned(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
DeviceVariant::Unknown {},
|
||||
props.get("node.description")?.to_owned(),
|
||||
)
|
||||
};
|
||||
|
||||
Some(Device {
|
||||
object_id: props.get("object.id")?.parse::<u32>().ok()?,
|
||||
variant,
|
||||
media_class: match props.get("media.class")? {
|
||||
"Audio/Sink" => MediaClass::Sink,
|
||||
"Audio/Source" => MediaClass::Source,
|
||||
_ => return None,
|
||||
},
|
||||
product_name,
|
||||
node_name: props.get("node.name")?.to_owned(),
|
||||
state: match info.state() {
|
||||
NodeState::Idle => DeviceState::Idle,
|
||||
NodeState::Running => DeviceState::Running,
|
||||
NodeState::Creating => DeviceState::Creating,
|
||||
NodeState::Suspended => DeviceState::Suspended,
|
||||
NodeState::Error(why) => DeviceState::Error(why.to_owned()),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Monitors the devices from a given ``PipeWire`` socket.
|
||||
///
|
||||
/// ``PipeWire`` sockets are found in `/run/user/{{UID}}/pipewire-0`.
|
||||
pub fn devices_from_socket(
|
||||
pw_cancel: pipewire::channel::Receiver<()>,
|
||||
mut on_event: futures::channel::mpsc::Sender<DeviceEvent>,
|
||||
) {
|
||||
let mut managed = BTreeMap::new();
|
||||
|
||||
let _res = nodes_from_socket(pw_cancel, move |main_loop, event| match event {
|
||||
NodeEvent::NodeInfo(pw_id, info) => {
|
||||
if let Some(device) = Device::from_node(info) {
|
||||
if managed.insert(pw_id, device.object_id).is_none() {
|
||||
if block_on(on_event.send(DeviceEvent::Add(device))).is_err() {
|
||||
main_loop.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeEvent::Remove(pw_id) => {
|
||||
if let Some(object_id) = managed.remove(&pw_id) {
|
||||
if block_on(on_event.send(DeviceEvent::Remove(object_id))).is_err() {
|
||||
main_loop.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Listens to information about nodes, passing that info into a callback.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Errors if the pipewire connection fails
|
||||
pub fn nodes_from_socket(
|
||||
pw_cancel: pipewire::channel::Receiver<()>,
|
||||
on_event: impl FnMut(&PwMainLoop, NodeEvent) + 'static,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let main_loop = PwMainLoop::new(None)?;
|
||||
let context = PwContext::new(&main_loop)?;
|
||||
let core = context.connect(None)?;
|
||||
|
||||
// Exit main loop on receivering terminate message.
|
||||
let _cancel_rx = pw_cancel.attach(main_loop.loop_(), {
|
||||
let main_loop = main_loop.clone();
|
||||
move |_| main_loop.quit()
|
||||
});
|
||||
|
||||
let registry = Rc::new(core.get_registry()?);
|
||||
let registry_weak = Rc::downgrade(®istry);
|
||||
|
||||
let proxies = Rc::new(RefCell::new(HashMap::new()));
|
||||
let on_event = Rc::new(RefCell::new(on_event));
|
||||
|
||||
let main_loop_clone = main_loop.clone();
|
||||
|
||||
let _registry_listener = registry
|
||||
.add_listener_local()
|
||||
.global(move |obj| {
|
||||
let Some(registry) = registry_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let attached_proxy: Option<(Box<dyn ProxyT>, Box<dyn Listener>)> = match obj.type_ {
|
||||
ObjectType::Node => {
|
||||
let Ok(node): Result<Node, _> = registry.bind(obj) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let on_event_weak = Rc::downgrade(&on_event);
|
||||
let main_loop = main_loop_clone.clone();
|
||||
let id = node.upcast_ref().id();
|
||||
|
||||
let listener = node
|
||||
.add_listener_local()
|
||||
.info(move |info| {
|
||||
if let Some(on_event) = on_event_weak.upgrade() {
|
||||
on_event.borrow_mut()(&main_loop, NodeEvent::NodeInfo(id, info));
|
||||
}
|
||||
})
|
||||
.register();
|
||||
|
||||
Some((Box::new(node), Box::new(listener)))
|
||||
}
|
||||
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some((proxy_spe, listener)) = attached_proxy {
|
||||
let proxy = proxy_spe.upcast_ref();
|
||||
let id = proxy.id();
|
||||
let (object_type, _object_version) = proxy.get_type();
|
||||
|
||||
let proxies_weak = Rc::downgrade(&proxies);
|
||||
let on_event_weak = Rc::downgrade(&on_event);
|
||||
let main_loop = main_loop_clone.clone();
|
||||
|
||||
let remove_listener = proxy
|
||||
.add_listener_local()
|
||||
.removed(move || {
|
||||
if object_type == ObjectType::Node {
|
||||
if let Some(on_event) = on_event_weak.upgrade() {
|
||||
on_event.borrow_mut()(&main_loop, NodeEvent::Remove(id));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(proxies) = proxies_weak.upgrade() {
|
||||
proxies.borrow_mut().remove(&id);
|
||||
}
|
||||
})
|
||||
.register();
|
||||
|
||||
proxies
|
||||
.borrow_mut()
|
||||
.insert(id, (proxy_spe, listener, remove_listener));
|
||||
}
|
||||
})
|
||||
.register();
|
||||
|
||||
main_loop.run();
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,752 +0,0 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Make sure not to fail if pulse not found, and reconnect?
|
||||
// change to device shouldn't send osd?
|
||||
|
||||
use cosmic::iced_futures::{self, Subscription, stream};
|
||||
use futures::{SinkExt, executor::block_on};
|
||||
use libpulse_binding::{
|
||||
callbacks::ListResult,
|
||||
channelmap::Map,
|
||||
context::{
|
||||
Context, FlagSet, State,
|
||||
introspect::{CardInfo, CardProfileInfo, Introspector, ServerInfo, SinkInfo, SourceInfo},
|
||||
subscribe::{Facility, InterestMaskSet, Operation},
|
||||
},
|
||||
def::{PortAvailable, Retval},
|
||||
mainloop::{
|
||||
api::MainloopApi,
|
||||
events::io::IoEventInternal,
|
||||
standard::{IterateResult, Mainloop},
|
||||
},
|
||||
volume::{ChannelVolumes, Volume},
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
convert::Infallible,
|
||||
io::{Read, Write},
|
||||
os::{
|
||||
fd::{FromRawFd, IntoRawFd, RawFd},
|
||||
raw::c_void,
|
||||
},
|
||||
rc::Rc,
|
||||
str::FromStr,
|
||||
sync::mpsc,
|
||||
};
|
||||
|
||||
pub fn subscription() -> iced_futures::Subscription<Event> {
|
||||
Subscription::run_with_id(
|
||||
"pulse",
|
||||
stream::channel(20, |sender| async {
|
||||
std::thread::spawn(move || thread(sender));
|
||||
futures::future::pending().await
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn thread(sender: futures::channel::mpsc::Sender<Event>) {
|
||||
let Some(mut main_loop) = Mainloop::new() else {
|
||||
log::error!("Failed to create PA main loop");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(mut context) = Context::new(&main_loop, "cosmic-osd") else {
|
||||
log::error!("Failed to create PA context");
|
||||
return;
|
||||
};
|
||||
|
||||
let data = Rc::new(Data {
|
||||
main_loop: RefCell::new(Mainloop {
|
||||
_inner: Rc::clone(&main_loop._inner),
|
||||
}),
|
||||
introspector: context.introspect(),
|
||||
sink_volume: Cell::new(None),
|
||||
sink_mute: Cell::new(None),
|
||||
source_volume: Cell::new(None),
|
||||
source_mute: Cell::new(None),
|
||||
default_sink_name: RefCell::new(None),
|
||||
default_source_name: RefCell::new(None),
|
||||
sender: RefCell::new(sender.clone()),
|
||||
});
|
||||
|
||||
let data_clone = data.clone();
|
||||
context.set_subscribe_callback(Some(Box::new(move |facility, operation, index| {
|
||||
data_clone.subscribe_cb(facility.unwrap(), operation, index);
|
||||
})));
|
||||
|
||||
let _ = context.connect(None, FlagSet::NOFAIL, None);
|
||||
|
||||
loop {
|
||||
if sender.is_closed() {
|
||||
return;
|
||||
}
|
||||
|
||||
match main_loop.iterate(false) {
|
||||
IterateResult::Success(_) => {}
|
||||
IterateResult::Err(_e) => {
|
||||
return;
|
||||
}
|
||||
IterateResult::Quit(_e) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if context.get_state() == State::Ready {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Inspect all available cards on startup
|
||||
data.introspector.get_card_info_list({
|
||||
let data_weak = Rc::downgrade(&data);
|
||||
move |card_info_res| {
|
||||
if let Some(data) = data_weak.upgrade() {
|
||||
data.card_info_cb(card_info_res)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
data.get_server_info();
|
||||
context.subscribe(
|
||||
InterestMaskSet::SERVER | InterestMaskSet::SINK | InterestMaskSet::SOURCE,
|
||||
|_| {},
|
||||
);
|
||||
|
||||
if let Err((err, retval)) = main_loop.run() {
|
||||
log::error!("PA main loop returned {:?}, error {}", retval, err);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event {
|
||||
Balance(Option<f32>),
|
||||
CardInfo(Card),
|
||||
DefaultSink(String),
|
||||
DefaultSource(String),
|
||||
SinkVolume(u32),
|
||||
Channels(PulseChannels),
|
||||
SinkMute(bool),
|
||||
SourceVolume(u32),
|
||||
SourceMute(bool),
|
||||
}
|
||||
|
||||
enum Request {
|
||||
Volume(u32, f32),
|
||||
Balance(u32, f32),
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PulseChannels {
|
||||
tx: mpsc::Sender<Request>,
|
||||
pipe_tx: std::fs::File,
|
||||
index: u32,
|
||||
}
|
||||
|
||||
impl Clone for PulseChannels {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
tx: self.tx.clone(),
|
||||
pipe_tx: self
|
||||
.pipe_tx
|
||||
.try_clone()
|
||||
.expect("failed to clone PulseChannels pipe writer"),
|
||||
index: self.index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data used by the [`handle_balance_io_new`] callback.
|
||||
struct HandleBalanceData(
|
||||
Context,
|
||||
ChannelVolumes,
|
||||
Map,
|
||||
std::sync::mpsc::Receiver<Request>,
|
||||
);
|
||||
|
||||
/// Callback for creating an IO event source [`MainloopApi::io_new`].
|
||||
extern "C" fn handle_balance_io_new(
|
||||
api: *const MainloopApi,
|
||||
event: *mut IoEventInternal,
|
||||
reader_fd: RawFd,
|
||||
_flags: libpulse_binding::mainloop::events::io::FlagSet,
|
||||
data: *mut c_void,
|
||||
) {
|
||||
// Take ownership of the data and borrow its contents.
|
||||
let mut data = unsafe { Box::<HandleBalanceData>::from_raw(data as _) };
|
||||
let HandleBalanceData(ctx, volumes, map, rx) = data.as_mut();
|
||||
|
||||
// Return early if the context is not ready, and give the data back.
|
||||
if ctx.get_state() != State::Ready {
|
||||
let _ = Box::leak(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the first byte cannot be read, destroy this event source with its reader and data.
|
||||
let mut buf = [0u8; 1];
|
||||
let mut reader = unsafe { std::fs::File::from_raw_fd(reader_fd) };
|
||||
if reader.read_exact(&mut buf).is_err() {
|
||||
(unsafe { &*api })
|
||||
.io_free
|
||||
.as_ref()
|
||||
.expect("io_free function is missing")(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Give ownership of the reader back.
|
||||
_ = reader.into_raw_fd();
|
||||
|
||||
while let Ok(req) = rx.try_recv() {
|
||||
match req {
|
||||
Request::Volume(index, volume_scale) => {
|
||||
let mut intro = ctx.introspect();
|
||||
|
||||
let new_scale = Volume((volume_scale * Volume::NORMAL.0 as f32).round() as u32);
|
||||
|
||||
if let Some(v) = volumes.scale(new_scale) {
|
||||
_ = intro.set_sink_volume_by_index(
|
||||
index,
|
||||
v,
|
||||
Some(Box::new(|success| {
|
||||
if !success {
|
||||
tracing::error!("Failed to set sink balance");
|
||||
}
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
Request::Balance(index, new_balance) => {
|
||||
if map.can_balance() {
|
||||
if let Some(v) = volumes.set_balance(&map, new_balance) {
|
||||
let mut intro = ctx.introspect();
|
||||
|
||||
_ = intro.set_sink_volume_by_index(
|
||||
index,
|
||||
v,
|
||||
Some(Box::new(|success| {
|
||||
if !success {
|
||||
tracing::error!("Failed to set sink balance");
|
||||
}
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Request::Quit => unsafe { &*api }
|
||||
.quit
|
||||
.as_ref()
|
||||
.expect("quit function missing")(api, 0),
|
||||
}
|
||||
}
|
||||
|
||||
let _ = Box::leak(data);
|
||||
}
|
||||
|
||||
impl PulseChannels {
|
||||
fn new(
|
||||
volumes: ChannelVolumes,
|
||||
map: Map,
|
||||
api: &MainloopApi,
|
||||
index: u32,
|
||||
ctx: Context,
|
||||
) -> PulseChannels {
|
||||
let (reader, writer) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC)
|
||||
.expect("failed to crate pipe");
|
||||
|
||||
let (tx, rx) = mpsc::channel::<Request>();
|
||||
|
||||
// Create IO event source object for handling speaker balance.
|
||||
let event_source = api.io_new.as_ref().unwrap()(
|
||||
api as *const _,
|
||||
reader.into_raw_fd(),
|
||||
libpulse_binding::mainloop::events::io::FlagSet::INPUT,
|
||||
Some(handle_balance_io_new),
|
||||
Box::into_raw(Box::new(HandleBalanceData(ctx, volumes, map, rx))) as *mut c_void,
|
||||
);
|
||||
|
||||
if let Some(enable) = api.io_enable.as_ref() {
|
||||
enable(
|
||||
event_source,
|
||||
libpulse_binding::mainloop::events::io::FlagSet::INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
tx,
|
||||
pipe_tx: std::fs::File::from(writer),
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the active index.
|
||||
#[inline]
|
||||
pub fn set_index(&mut self, index: u32) {
|
||||
self.index = index;
|
||||
}
|
||||
|
||||
/// Set the speaker balance of the active sink.
|
||||
pub fn set_balance(&mut self, balance: f32) {
|
||||
if let Err(err) = self.tx.send(Request::Balance(self.index, balance)) {
|
||||
tracing::error!(?err, "Failed to send new balance to channel");
|
||||
} else {
|
||||
self.pipe_tx
|
||||
.write_all(&[1])
|
||||
.expect("PulseChannels pipe write failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the volume of the active sink.
|
||||
pub fn set_volume(&mut self, volume: f32) {
|
||||
if let Err(err) = self.tx.send(Request::Volume(self.index, volume)) {
|
||||
tracing::error!(?err, "Failed to send new volume to channel");
|
||||
} else {
|
||||
self.pipe_tx
|
||||
.write_all(&[1])
|
||||
.expect("PulseChannels pipe write failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Request the pulse thread to quit.
|
||||
pub fn quit(mut self) {
|
||||
_ = self.tx.send(Request::Quit);
|
||||
_ = self.pipe_tx.write_all(&[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct Card {
|
||||
pub object_id: u32,
|
||||
pub name: String,
|
||||
pub product_name: String,
|
||||
pub variant: DeviceVariant,
|
||||
pub ports: Vec<CardPort>,
|
||||
pub profiles: Vec<CardProfile>,
|
||||
pub active_profile: Option<CardProfile>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct CardPort {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub direction: Direction,
|
||||
pub port_type: PortType,
|
||||
pub profile_port: u32,
|
||||
pub priority: u32,
|
||||
pub profiles: Vec<CardProfile>,
|
||||
pub availability: Availability,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum Availability {
|
||||
Unknown,
|
||||
No,
|
||||
Yes,
|
||||
}
|
||||
|
||||
impl From<PortAvailable> for Availability {
|
||||
fn from(pa: PortAvailable) -> Self {
|
||||
match pa {
|
||||
PortAvailable::Unknown => Availability::Unknown,
|
||||
PortAvailable::No => Availability::No,
|
||||
PortAvailable::Yes => Availability::Yes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct CardProfile {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub available: bool,
|
||||
pub n_sinks: u32,
|
||||
pub n_sources: u32,
|
||||
pub priority: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum DeviceVariant {
|
||||
Alsa { alsa_card: u32 },
|
||||
Bluez5 { address: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum Direction {
|
||||
Input,
|
||||
Output,
|
||||
Both,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum PortType {
|
||||
Mic,
|
||||
Speaker,
|
||||
Headphones,
|
||||
Headset,
|
||||
Digital,
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl FromStr for PortType {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"mic" => Ok(PortType::Mic),
|
||||
"speaker" => Ok(PortType::Speaker),
|
||||
"headphones" => Ok(PortType::Headphones),
|
||||
"headset" => Ok(PortType::Headset),
|
||||
"digital" => Ok(PortType::Digital),
|
||||
_ => Ok(PortType::Unknown),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Data {
|
||||
main_loop: RefCell<Mainloop>,
|
||||
default_sink_name: RefCell<Option<String>>,
|
||||
default_source_name: RefCell<Option<String>>,
|
||||
sink_volume: Cell<Option<u32>>,
|
||||
sink_mute: Cell<Option<bool>>,
|
||||
source_volume: Cell<Option<u32>>,
|
||||
source_mute: Cell<Option<bool>>,
|
||||
introspector: Introspector,
|
||||
sender: RefCell<futures::channel::mpsc::Sender<Event>>,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
fn card_info_cb(self: &Rc<Self>, card_info: ListResult<&CardInfo>) {
|
||||
if let ListResult::Item(card_info) = card_info {
|
||||
let Some(object_id) = card_info
|
||||
.proplist
|
||||
.get_str("object.id")
|
||||
.and_then(|v| v.parse::<u32>().ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let variant = if let Some(alsa_card) = card_info
|
||||
.proplist
|
||||
.get_str("alsa.card")
|
||||
.and_then(|v| v.parse::<u32>().ok())
|
||||
{
|
||||
DeviceVariant::Alsa { alsa_card }
|
||||
} else if let Some(address) = card_info.proplist.get_str("api.bluez5.address") {
|
||||
DeviceVariant::Bluez5 { address }
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let card = Card {
|
||||
name: card_info
|
||||
.name
|
||||
.as_ref()
|
||||
.map(Cow::to_string)
|
||||
.unwrap_or_default(),
|
||||
product_name: card_info
|
||||
.proplist
|
||||
.get_str("device.product.name")
|
||||
.unwrap_or_default(),
|
||||
object_id,
|
||||
variant,
|
||||
ports: card_info
|
||||
.ports
|
||||
.iter()
|
||||
.map(|port| CardPort {
|
||||
name: port.name.as_ref().map(Cow::to_string).unwrap_or_default(),
|
||||
description: port
|
||||
.description
|
||||
.as_ref()
|
||||
.map(Cow::to_string)
|
||||
.unwrap_or_default(),
|
||||
direction: match port.direction.bits() {
|
||||
x if x == libpulse_binding::direction::FlagSet::INPUT.bits() => {
|
||||
Direction::Input
|
||||
}
|
||||
x if x == libpulse_binding::direction::FlagSet::OUTPUT.bits() => {
|
||||
Direction::Output
|
||||
}
|
||||
_ => Direction::Both,
|
||||
},
|
||||
port_type: port
|
||||
.proplist
|
||||
.get_str("port.type")
|
||||
.as_deref()
|
||||
.map(|s| PortType::from_str(s).unwrap())
|
||||
.unwrap_or_default(),
|
||||
profile_port: port
|
||||
.proplist
|
||||
.get_str("card.profile.port")
|
||||
.and_then(|v| v.parse::<u32>().ok())
|
||||
.unwrap_or(0),
|
||||
priority: port.priority,
|
||||
profiles: collect_profiles(&port.profiles),
|
||||
availability: port.available.into(),
|
||||
})
|
||||
.collect(),
|
||||
profiles: collect_profiles(&card_info.profiles),
|
||||
active_profile: card_info.active_profile.as_deref().map(CardProfile::from),
|
||||
};
|
||||
|
||||
if block_on(self.sender.borrow_mut().send(Event::CardInfo(card))).is_err() {
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn server_info_cb(self: &Rc<Self>, server_info: &ServerInfo) {
|
||||
let new_default_sink_name = server_info
|
||||
.default_sink_name
|
||||
.as_ref()
|
||||
.map(|x| x.clone().into_owned());
|
||||
let mut default_sink_name = self.default_sink_name.borrow_mut();
|
||||
if new_default_sink_name != *default_sink_name {
|
||||
if let Some(name) = &new_default_sink_name {
|
||||
_ = block_on(
|
||||
self.sender
|
||||
.borrow_mut()
|
||||
.send(Event::DefaultSink(name.clone())),
|
||||
);
|
||||
self.get_sink_info_by_name(name);
|
||||
}
|
||||
*default_sink_name = new_default_sink_name;
|
||||
}
|
||||
|
||||
let new_default_source_name = server_info
|
||||
.default_source_name
|
||||
.as_ref()
|
||||
.map(|x| x.clone().into_owned());
|
||||
let mut default_source_name = self.default_source_name.borrow_mut();
|
||||
if new_default_source_name != *default_source_name {
|
||||
if let Some(name) = &new_default_source_name {
|
||||
_ = block_on(
|
||||
self.sender
|
||||
.borrow_mut()
|
||||
.send(Event::DefaultSource(name.clone())),
|
||||
);
|
||||
self.get_source_info_by_name(name);
|
||||
}
|
||||
*default_source_name = new_default_source_name;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_server_info(self: &Rc<Self>) {
|
||||
let data = self.clone();
|
||||
self.introspector
|
||||
.get_server_info(move |server_info| data.server_info_cb(server_info));
|
||||
}
|
||||
|
||||
fn sink_info_cb(&self, sink_info_res: ListResult<&SinkInfo>) {
|
||||
if let ListResult::Item(sink_info) = sink_info_res {
|
||||
if sink_info.name.as_deref() != self.default_sink_name.borrow().as_deref() {
|
||||
return;
|
||||
}
|
||||
let balance = (sink_info.channel_map.can_balance()
|
||||
&& sink_info.base_volume.is_normal())
|
||||
.then(|| sink_info.volume.get_balance(&sink_info.channel_map));
|
||||
|
||||
let volume = sink_info.volume.max().0 / (Volume::NORMAL.0 / 100);
|
||||
if self.sink_mute.get() != Some(sink_info.mute) {
|
||||
self.sink_mute.set(Some(sink_info.mute));
|
||||
if block_on(
|
||||
self.sender
|
||||
.borrow_mut()
|
||||
.send(Event::SinkMute(sink_info.mute)),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
}
|
||||
if self.sink_volume.get() != Some(volume) {
|
||||
self.sink_volume.set(Some(volume));
|
||||
if block_on(self.sender.borrow_mut().send(Event::SinkVolume(volume))).is_err() {
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
}
|
||||
if block_on(self.sender.borrow_mut().send(Event::Balance(balance))).is_err() {
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
let mut main_loop = self.main_loop.borrow_mut();
|
||||
let api = main_loop.get_api();
|
||||
if let Some(mut ctx) = Context::new(&*main_loop, "balance") {
|
||||
let _ = ctx.connect(None, FlagSet::NOFAIL, None);
|
||||
|
||||
let channels = PulseChannels::new(
|
||||
sink_info.volume,
|
||||
sink_info.channel_map,
|
||||
api,
|
||||
sink_info.index,
|
||||
ctx,
|
||||
);
|
||||
|
||||
if block_on(self.sender.borrow_mut().send(Event::Channels(channels))).is_err() {
|
||||
main_loop.quit(Retval(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn source_info_cb(&self, source_info_res: ListResult<&SourceInfo>) {
|
||||
if let ListResult::Item(source_info) = source_info_res {
|
||||
if source_info.name.as_deref() != self.default_source_name.borrow().as_deref() {
|
||||
return;
|
||||
}
|
||||
let volume = source_info.volume.max().0 / (Volume::NORMAL.0 / 100);
|
||||
if self.source_mute.get() != Some(source_info.mute) {
|
||||
self.source_mute.set(Some(source_info.mute));
|
||||
if block_on(
|
||||
self.sender
|
||||
.borrow_mut()
|
||||
.send(Event::SourceMute(source_info.mute)),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
}
|
||||
if self.source_volume.get() != Some(volume) {
|
||||
self.source_volume.set(Some(volume));
|
||||
if block_on(self.sender.borrow_mut().send(Event::SourceVolume(volume))).is_err() {
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_card_info_by_index(self: &Rc<Self>, index: u32) {
|
||||
let data = self.clone();
|
||||
self.introspector
|
||||
.get_card_info_by_index(index, move |card_info_res| {
|
||||
data.card_info_cb(card_info_res);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_sink_info_by_index(self: &Rc<Self>, index: u32) {
|
||||
let data = self.clone();
|
||||
self.introspector.get_sink_info_by_index(
|
||||
index,
|
||||
move |sink_info_res: ListResult<&SinkInfo<'_>>| {
|
||||
if let ListResult::Item(ref info) = sink_info_res {
|
||||
if let Some(card_index) = info.card {
|
||||
let data_clone = data.clone();
|
||||
data.introspector.get_card_info_by_index(
|
||||
card_index,
|
||||
move |card_info_res| {
|
||||
data_clone.card_info_cb(card_info_res);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
data.sink_info_cb(sink_info_res);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn get_sink_info_by_name(self: &Rc<Self>, name: &str) {
|
||||
let data = self.clone();
|
||||
self.introspector
|
||||
.get_sink_info_by_name(name, move |sink_info_res| {
|
||||
if let ListResult::Item(ref info) = sink_info_res {
|
||||
if let Some(card_index) = info.card {
|
||||
let data_clone = data.clone();
|
||||
data.introspector.get_card_info_by_index(
|
||||
card_index,
|
||||
move |card_info_res| {
|
||||
data_clone.card_info_cb(card_info_res);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
data.sink_info_cb(sink_info_res);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_source_info_by_index(self: &Rc<Self>, index: u32) {
|
||||
let data = self.clone();
|
||||
self.introspector
|
||||
.get_source_info_by_index(index, move |source_info_res| {
|
||||
if let ListResult::Item(ref info) = source_info_res {
|
||||
if let Some(card_index) = info.card {
|
||||
let data_clone = data.clone();
|
||||
data.introspector.get_card_info_by_index(
|
||||
card_index,
|
||||
move |card_info_res| {
|
||||
data_clone.card_info_cb(card_info_res);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
data.source_info_cb(source_info_res);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_source_info_by_name(self: &Rc<Self>, name: &str) {
|
||||
let data = self.clone();
|
||||
self.introspector
|
||||
.get_source_info_by_name(name, move |source_info_res| {
|
||||
if let ListResult::Item(ref info) = source_info_res {
|
||||
if let Some(card_index) = info.card {
|
||||
let data_clone = data.clone();
|
||||
data.introspector.get_card_info_by_index(
|
||||
card_index,
|
||||
move |card_info_res| {
|
||||
data_clone.card_info_cb(card_info_res);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
data.source_info_cb(source_info_res);
|
||||
});
|
||||
}
|
||||
|
||||
fn subscribe_cb(
|
||||
self: &Rc<Self>,
|
||||
facility: Facility,
|
||||
_operation: Option<Operation>,
|
||||
index: u32,
|
||||
) {
|
||||
match facility {
|
||||
Facility::Server => {
|
||||
self.get_server_info();
|
||||
}
|
||||
Facility::Sink => {
|
||||
self.get_sink_info_by_index(index);
|
||||
}
|
||||
Facility::Source => {
|
||||
self.get_source_info_by_index(index);
|
||||
}
|
||||
Facility::Card => {
|
||||
self.get_card_info_by_index(index);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_profiles(profiles: &[CardProfileInfo]) -> Vec<CardProfile> {
|
||||
profiles.iter().map(CardProfile::from).collect()
|
||||
}
|
||||
|
||||
impl From<&CardProfileInfo<'_>> for CardProfile {
|
||||
fn from(profile: &CardProfileInfo) -> Self {
|
||||
CardProfile {
|
||||
name: profile
|
||||
.name
|
||||
.as_ref()
|
||||
.map(Cow::to_string)
|
||||
.unwrap_or_default(),
|
||||
description: profile
|
||||
.description
|
||||
.as_ref()
|
||||
.map(Cow::to_string)
|
||||
.unwrap_or_default(),
|
||||
available: profile.available,
|
||||
n_sinks: profile.n_sinks,
|
||||
n_sources: profile.n_sources,
|
||||
priority: profile.priority,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings-upower-subscription"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0-beta6"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version.workspace = true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue