feat: add sound settings page
This commit is contained in:
parent
66f32168ca
commit
7cf99665d6
6 changed files with 738 additions and 51 deletions
233
Cargo.lock
generated
233
Cargo.lock
generated
|
|
@ -161,6 +161,16 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotate-snippets"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"yansi-term",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
|
|
@ -661,6 +671,27 @@ version = "0.22.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.69.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
|
||||
dependencies = [
|
||||
"annotate-snippets",
|
||||
"bitflags 2.6.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.12.1",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.3"
|
||||
|
|
@ -966,6 +997,15 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfb"
|
||||
version = "0.7.3"
|
||||
|
|
@ -1020,6 +1060,17 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.11"
|
||||
|
|
@ -1282,6 +1333,24 @@ dependencies = [
|
|||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
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"
|
||||
version = "0.9.4"
|
||||
|
|
@ -1472,6 +1541,7 @@ dependencies = [
|
|||
"cosmic-randr-shell",
|
||||
"cosmic-settings-config",
|
||||
"cosmic-settings-page",
|
||||
"cosmic-settings-subscriptions",
|
||||
"cosmic-settings-system",
|
||||
"cosmic-settings-wallpaper",
|
||||
"derivative",
|
||||
|
|
@ -1479,7 +1549,7 @@ dependencies = [
|
|||
"dirs",
|
||||
"downcast-rs",
|
||||
"freedesktop-desktop-entry",
|
||||
"futures-lite 2.3.0",
|
||||
"futures",
|
||||
"generator 0.8.1",
|
||||
"hostname-validator",
|
||||
"hostname1-zbus",
|
||||
|
|
@ -1548,6 +1618,23 @@ dependencies = [
|
|||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-settings-subscriptions"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/cosmic-settings-subscriptions?branch=sound-settings#177d4781f1e976ac62af6d6f3f193f96c1544d76"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"iced_futures",
|
||||
"libpulse-binding",
|
||||
"log",
|
||||
"pipewire",
|
||||
"rustix 0.38.34",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"upower_dbus",
|
||||
"zbus 4.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-settings-system"
|
||||
version = "0.1.0"
|
||||
|
|
@ -2598,6 +2685,12 @@ version = "0.24.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
version = "0.13.1"
|
||||
|
|
@ -3892,6 +3985,12 @@ 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.2"
|
||||
|
|
@ -3982,6 +4081,33 @@ version = "0.2.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
|
||||
[[package]]
|
||||
name = "libpulse-binding"
|
||||
version = "2.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
"libpulse-sys",
|
||||
"num-derive 0.3.3",
|
||||
"num-traits",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libpulse-sys"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"num-derive 0.3.3",
|
||||
"num-traits",
|
||||
"pkg-config",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.0.2"
|
||||
|
|
@ -4003,6 +4129,34 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libspa"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cc",
|
||||
"convert_case",
|
||||
"cookie-factory",
|
||||
"libc",
|
||||
"libspa-sys",
|
||||
"nix 0.27.1",
|
||||
"nom",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libspa-sys"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
|
|
@ -4378,6 +4532,17 @@ 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.6.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
|
|
@ -4484,6 +4649,17 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
|
|
@ -4885,6 +5061,34 @@ dependencies = [
|
|||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pipewire"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.6.0",
|
||||
"libc",
|
||||
"libspa",
|
||||
"libspa-sys",
|
||||
"nix 0.27.1",
|
||||
"once_cell",
|
||||
"pipewire-sys",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pipewire-sys"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"libspa-sys",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
|
|
@ -5156,7 +5360,7 @@ dependencies = [
|
|||
"maybe-rayon",
|
||||
"new_debug_unreachable",
|
||||
"noop_proc_macro",
|
||||
"num-derive",
|
||||
"num-derive 0.4.2",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"paste",
|
||||
|
|
@ -5768,6 +5972,12 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
|
|
@ -6740,6 +6950,16 @@ version = "0.2.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "upower_dbus"
|
||||
version = "0.3.2"
|
||||
source = "git+https://github.com/pop-os/dbus-settings-bindings#cd21ddcb1b5cbfc80ab84b34d3c8b1ff3d81179a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"zbus 4.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
|
|
@ -7786,6 +8006,15 @@ version = "0.8.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984"
|
||||
|
||||
[[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.1.6"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ derive_setters = "0.1.6"
|
|||
dirs = "5.0.1"
|
||||
downcast-rs = "1.2.1"
|
||||
freedesktop-desktop-entry = "0.7.0"
|
||||
futures = { package = "futures-lite", version = "2.3.0" }
|
||||
futures = "0.3.30"
|
||||
generator = "=0.8.1"
|
||||
hostname-validator = "1.1.1"
|
||||
hostname1-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
||||
|
|
@ -55,6 +55,12 @@ zbus = { version = "4.4.0", features = ["tokio"] }
|
|||
tachyonix = "0.3.0"
|
||||
slab = "0.4.9"
|
||||
|
||||
[dependencies.cosmic-settings-subscriptions]
|
||||
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
|
||||
branch = "sound-settings"
|
||||
# path = "../../cosmic-settings-subscriptions"
|
||||
features = ["pipewire", "pulse"]
|
||||
|
||||
[dependencies.icu]
|
||||
version = "1.4.0"
|
||||
features = ["experimental", "compiled_data", "icu_datetime_experimental"]
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ impl cosmic::Application for SettingsApp {
|
|||
|
||||
let desktop_id = app.insert_page::<desktop::Page>().id();
|
||||
app.insert_page::<display::Page>();
|
||||
//app.insert_page::<sound::Page>();
|
||||
app.insert_page::<sound::Page>();
|
||||
app.insert_page::<power::Page>();
|
||||
app.insert_page::<input::Page>();
|
||||
app.insert_page::<time::Page>();
|
||||
|
|
@ -381,6 +381,12 @@ impl cosmic::Application for SettingsApp {
|
|||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::Sound(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<sound::Page>() {
|
||||
return page.update(message).map(Into::into);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::SystemShortcuts(message) => {
|
||||
if let Some(page) = self
|
||||
.pages
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ pub enum Message {
|
|||
Panel(desktop::panel::Message),
|
||||
PanelApplet(desktop::panel::applets_inner::Message),
|
||||
Power(power::Message),
|
||||
Sound(sound::Message),
|
||||
SystemShortcuts(input::keyboard::shortcuts::ShortcutMessage),
|
||||
TilingShortcuts(input::keyboard::shortcuts::ShortcutMessage),
|
||||
WindowManagement(desktop::window_management::Message),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,82 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::widget::{settings, text};
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use cosmic::{
|
||||
widget::{self, settings},
|
||||
Apply, Command, Element,
|
||||
};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use cosmic_settings_subscriptions::{pipewire, pulse};
|
||||
use futures::StreamExt;
|
||||
use slab::Slab;
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
// Get default sinks/sources and their volumes/mute status.
|
||||
Pulse(pulse::Event),
|
||||
// Get ALSA cards and their profiles.
|
||||
Pipewire(pipewire::DeviceEvent),
|
||||
// Change the default microphone input.
|
||||
SinkChanged(usize),
|
||||
// Request to change the default microphone volume.
|
||||
SinkVolumeChanged(u32),
|
||||
// Change the microphone volume.
|
||||
SinkVolumeApply(NodeId),
|
||||
// Toggle the mute status of the microphone.
|
||||
SinkMuteToggle,
|
||||
// Change the default speaker output.
|
||||
SourceChanged(usize),
|
||||
// Request to change the speaker volume.
|
||||
SourceVolumeChanged(u32),
|
||||
// Change the speaker volume.
|
||||
SourceVolumeApply(NodeId),
|
||||
// Toggle the mute status of the speaker output.
|
||||
SourceMuteToggle,
|
||||
}
|
||||
|
||||
pub type CardId = u32;
|
||||
pub type NodeId = u32;
|
||||
pub type ProfileId = u32;
|
||||
|
||||
struct Card {
|
||||
class: pipewire::MediaClass,
|
||||
// name: String,
|
||||
profiles: HashMap<NodeId, Profile>,
|
||||
}
|
||||
|
||||
struct Profile {
|
||||
// device: ProfileId,
|
||||
identifier: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
pub struct Page {
|
||||
pipewire_thread: Option<(tokio::sync::oneshot::Sender<()>, pipewire::Sender<()>)>,
|
||||
pulse_thread: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
alsa_cards: HashMap<CardId, Card>,
|
||||
default_sink: String,
|
||||
default_source: String,
|
||||
|
||||
sink_volume: u32,
|
||||
sink_volume_text: String,
|
||||
sink_mute: bool,
|
||||
sink_volume_debounce: bool,
|
||||
|
||||
source_volume: u32,
|
||||
source_volume_text: String,
|
||||
source_mute: bool,
|
||||
source_volume_debounce: bool,
|
||||
|
||||
sinks: Vec<String>,
|
||||
sink_ids: Vec<NodeId>,
|
||||
sources: Vec<String>,
|
||||
source_ids: Vec<NodeId>,
|
||||
active_sink: Option<usize>,
|
||||
active_source: Option<usize>,
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn content(
|
||||
|
|
@ -17,8 +86,8 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
Some(vec![
|
||||
sections.insert(output()),
|
||||
sections.insert(input()),
|
||||
sections.insert(alerts()),
|
||||
sections.insert(applications()),
|
||||
// sections.insert(alerts()),
|
||||
// sections.insert(applications()),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -27,42 +96,299 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
.title(fl!("sound"))
|
||||
.description(fl!("sound", "desc"))
|
||||
}
|
||||
|
||||
fn on_enter(
|
||||
&mut self,
|
||||
_page: cosmic_settings_page::Entity,
|
||||
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
) -> Command<crate::pages::Message> {
|
||||
if self.pulse_thread.is_none() {
|
||||
let sender = sender.clone();
|
||||
|
||||
let (tx, mut rx) = futures::channel::mpsc::channel(1);
|
||||
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
// Listen to events from the pulse thread until the tx channel is closed.
|
||||
_ = std::thread::spawn(move || {
|
||||
pulse::thread(tx);
|
||||
});
|
||||
|
||||
// Forward events from the pulse thread to the application until
|
||||
// the application requests to stop listening to the pulse thread.
|
||||
tokio::task::spawn(async move {
|
||||
let forwarder = std::pin::pin!(async move {
|
||||
while let Some(event) = rx.next().await {
|
||||
let event = crate::pages::Message::Sound(Message::Pulse(event));
|
||||
if sender.send(event).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
futures::future::select(std::pin::pin!(cancel_rx), forwarder).await;
|
||||
});
|
||||
|
||||
self.pulse_thread = Some(cancel_tx);
|
||||
}
|
||||
|
||||
if self.pipewire_thread.is_none() {
|
||||
let (tx, mut rx) = futures::channel::mpsc::channel(1);
|
||||
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
// Listen to events from the pipewire thread until the tx channel is closed.
|
||||
let (_handle, terminate) = pipewire::thread(tx);
|
||||
|
||||
// Forward events from the pipewire thread to the application until
|
||||
// the application requests to stop listening to the pulse thread.
|
||||
tokio::task::spawn(async move {
|
||||
let forwarder = std::pin::pin!(async move {
|
||||
while let Some(event) = rx.next().await {
|
||||
let event = crate::pages::Message::Sound(Message::Pipewire(event));
|
||||
if sender.send(event).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
futures::future::select(std::pin::pin!(cancel_rx), forwarder).await;
|
||||
});
|
||||
|
||||
self.pipewire_thread = Some((cancel_tx, terminate));
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> Command<crate::pages::Message> {
|
||||
if let Some(cancellation) = self.pulse_thread.take() {
|
||||
_ = cancellation.send(());
|
||||
}
|
||||
|
||||
if let Some((cancellation, terminate)) = self.pipewire_thread.take() {
|
||||
_ = cancellation.send(());
|
||||
_ = terminate.send(());
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
fn alerts() -> Section<crate::pages::Message> {
|
||||
let mut descriptions = Slab::new();
|
||||
let volume = descriptions.insert(fl!("sound-alerts", "volume"));
|
||||
let sound = descriptions.insert(fl!("sound-alerts", "sound"));
|
||||
impl Page {
|
||||
fn set_default_sink(&mut self) {
|
||||
for card in self.alsa_cards.values() {
|
||||
if let pipewire::MediaClass::Sink = card.class {
|
||||
for (&node_id, profile) in &card.profiles {
|
||||
if profile.identifier == self.default_sink {
|
||||
self.active_sink = self.sink_ids.iter().position(|&id| id == node_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section::default()
|
||||
.title(fl!("sound-alerts"))
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(§ion.descriptions[volume], text("TODO")))
|
||||
.add(settings::item(§ion.descriptions[sound], text("TODO")))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
fn set_default_source(&mut self) {
|
||||
for card in self.alsa_cards.values() {
|
||||
if let pipewire::MediaClass::Source = card.class {
|
||||
for (&node_id, profile) in &card.profiles {
|
||||
if profile.identifier == self.default_source {
|
||||
self.active_source = self.source_ids.iter().position(|&id| id == node_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn applications() -> Section<crate::pages::Message> {
|
||||
let mut descriptions = Slab::new();
|
||||
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
|
||||
match message {
|
||||
Message::SourceVolumeChanged(volume) => {
|
||||
self.source_volume = volume;
|
||||
self.source_volume_text = volume.to_string();
|
||||
if self.source_volume_debounce {
|
||||
return Command::none();
|
||||
}
|
||||
|
||||
let applications = descriptions.insert(fl!("sound-applications", "desc"));
|
||||
let mut command = None;
|
||||
if let Some(&node_id) = self.source_ids.get(self.active_source.unwrap_or(0)) {
|
||||
command = Some(cosmic::command::future(async move {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
crate::pages::Message::Sound(Message::SourceVolumeApply(node_id))
|
||||
}));
|
||||
}
|
||||
|
||||
Section::default()
|
||||
.title(fl!("sound-applications"))
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
&*section.descriptions[applications],
|
||||
text("TODO"),
|
||||
))
|
||||
.into()
|
||||
})
|
||||
if let Some(command) = command {
|
||||
self.source_volume_debounce = true;
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
Message::Pulse(pulse::Event::SourceVolume(volume)) => {
|
||||
if self.sink_volume_debounce {
|
||||
return Command::none();
|
||||
}
|
||||
|
||||
self.source_volume = volume;
|
||||
self.source_volume_text = volume.to_string();
|
||||
}
|
||||
|
||||
Message::SinkVolumeChanged(volume) => {
|
||||
self.sink_volume = volume;
|
||||
self.sink_volume_text = volume.to_string();
|
||||
if self.sink_volume_debounce {
|
||||
return Command::none();
|
||||
}
|
||||
|
||||
let mut command = None;
|
||||
if let Some(&node_id) = self.sink_ids.get(self.active_sink.unwrap_or(0)) {
|
||||
command = Some(cosmic::command::future(async move {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
crate::pages::Message::Sound(Message::SinkVolumeApply(node_id))
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(command) = command {
|
||||
self.source_volume_debounce = true;
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
Message::Pulse(pulse::Event::SinkVolume(volume)) => {
|
||||
if self.sink_volume_debounce {
|
||||
return Command::none();
|
||||
}
|
||||
|
||||
self.sink_volume = volume;
|
||||
self.sink_volume_text = volume.to_string();
|
||||
}
|
||||
|
||||
Message::Pulse(pulse::Event::DefaultSink(sink)) => {
|
||||
self.default_sink = sink;
|
||||
self.set_default_sink();
|
||||
}
|
||||
|
||||
Message::Pulse(pulse::Event::DefaultSource(source)) => {
|
||||
self.default_source = source;
|
||||
self.set_default_source();
|
||||
}
|
||||
|
||||
Message::Pulse(pulse::Event::SinkMute(mute)) => {
|
||||
self.sink_mute = mute;
|
||||
}
|
||||
|
||||
Message::Pulse(pulse::Event::SourceMute(mute)) => {
|
||||
self.source_mute = mute;
|
||||
}
|
||||
|
||||
Message::Pipewire(pipewire::DeviceEvent::Add(device)) => {
|
||||
match device.media_class {
|
||||
pipewire::MediaClass::Sink => {
|
||||
self.sinks.push(device.node_description.clone());
|
||||
self.sink_ids.push(device.object_id);
|
||||
if self.default_sink == device.node_name {
|
||||
self.active_sink = Some(self.sinks.len() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
pipewire::MediaClass::Source => {
|
||||
self.sources.push(device.node_description.clone());
|
||||
self.source_ids.push(device.object_id);
|
||||
if self.default_source == device.node_name {
|
||||
self.active_source = Some(self.sources.len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let card = self
|
||||
.alsa_cards
|
||||
.entry(device.alsa_card)
|
||||
.or_insert_with(|| Card {
|
||||
class: device.media_class,
|
||||
// name: device.alsa_card_name,
|
||||
profiles: HashMap::new(),
|
||||
});
|
||||
|
||||
card.profiles.insert(
|
||||
device.object_id,
|
||||
Profile {
|
||||
// device: device.card_profile_device,
|
||||
identifier: device.node_name,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Message::Pipewire(pipewire::DeviceEvent::Remove(device_id)) => {
|
||||
let mut remove = None;
|
||||
for (card_id, card) in &mut self.alsa_cards {
|
||||
if card.profiles.remove(&device_id).is_some() {
|
||||
if card.profiles.is_empty() {
|
||||
remove = Some(*card_id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(card_id) = remove {
|
||||
_ = self.alsa_cards.remove(&card_id);
|
||||
}
|
||||
|
||||
if let Some(pos) = self.sink_ids.iter().position(|&id| id == device_id) {
|
||||
_ = self.sink_ids.remove(pos);
|
||||
_ = self.sinks.remove(pos);
|
||||
if self.active_sink == Some(pos) {
|
||||
self.active_sink = None;
|
||||
}
|
||||
} else if let Some(pos) = self.source_ids.iter().position(|&id| id == device_id) {
|
||||
_ = self.source_ids.remove(pos);
|
||||
_ = self.sources.remove(pos);
|
||||
if self.active_source == Some(pos) {
|
||||
self.active_source = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::SinkChanged(pos) => {
|
||||
if let Some(&node_id) = self.sink_ids.get(pos) {
|
||||
self.active_sink = Some(pos);
|
||||
wpctl_set_default(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
Message::SourceChanged(pos) => {
|
||||
if let Some(&node_id) = self.sink_ids.get(pos) {
|
||||
self.active_source = Some(pos);
|
||||
wpctl_set_default(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
Message::SinkVolumeApply(node_id) => {
|
||||
self.sink_volume_debounce = false;
|
||||
wpctl_set_volume(node_id, self.sink_volume);
|
||||
}
|
||||
|
||||
Message::SourceVolumeApply(node_id) => {
|
||||
self.source_volume_debounce = false;
|
||||
wpctl_set_volume(node_id, self.source_volume);
|
||||
}
|
||||
|
||||
Message::SinkMuteToggle => {
|
||||
self.sink_mute = !self.sink_mute;
|
||||
if let Some(&node_id) = self.sink_ids.get(self.active_sink.unwrap_or(0)) {
|
||||
wpctl_set_mute(node_id, self.sink_mute);
|
||||
}
|
||||
}
|
||||
|
||||
Message::SourceMuteToggle => {
|
||||
self.source_mute = !self.source_mute;
|
||||
if let Some(&node_id) = self.source_ids.get(self.active_source.unwrap_or(0)) {
|
||||
wpctl_set_mute(node_id, self.source_mute);
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
fn input() -> Section<crate::pages::Message> {
|
||||
|
|
@ -70,17 +396,44 @@ 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 _level = descriptions.insert(fl!("sound-input", "level"));
|
||||
|
||||
Section::default()
|
||||
.title(fl!("sound-input"))
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, _page, section| {
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let volume_control = widget::row::with_capacity(3)
|
||||
.align_items(cosmic::iced::Alignment::Center)
|
||||
.spacing(4)
|
||||
.push(
|
||||
widget::button::icon(if page.source_mute {
|
||||
widget::icon::from_name("microphone-sensitivity-muted-symbolic")
|
||||
} else {
|
||||
widget::icon::from_name("audio-input-microphone-symbolic")
|
||||
})
|
||||
.on_press(Message::SourceMuteToggle),
|
||||
)
|
||||
.push(widget::text::body(&page.source_volume_text))
|
||||
.push(
|
||||
widget::slider(0..=150, page.source_volume, Message::SourceVolumeChanged)
|
||||
.width(250)
|
||||
.breakpoints(&[100]),
|
||||
);
|
||||
|
||||
let devices = widget::dropdown(
|
||||
&page.sources,
|
||||
Some(page.active_source.unwrap_or(0)),
|
||||
Message::SourceChanged,
|
||||
);
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&*section.descriptions[volume], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[device], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[level], text("TODO")))
|
||||
.into()
|
||||
.add(settings::item(
|
||||
&*section.descriptions[volume],
|
||||
volume_control,
|
||||
))
|
||||
.add(settings::item(&*section.descriptions[device], devices))
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Sound)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -89,19 +442,110 @@ 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 config = descriptions.insert(fl!("sound-output", "config"));
|
||||
let _level = descriptions.insert(fl!("sound-output", "level"));
|
||||
// let config = descriptions.insert(fl!("sound-output", "config"));
|
||||
// let balance = descriptions.insert(fl!("sound-output", "balance"));
|
||||
|
||||
Section::default()
|
||||
.title(fl!("sound-output"))
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, _page, section| {
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let volume_control = widget::row::with_capacity(3)
|
||||
.align_items(cosmic::iced::Alignment::Center)
|
||||
.spacing(4)
|
||||
.push(
|
||||
widget::button::icon(if page.sink_mute {
|
||||
widget::icon::from_name("audio-volume-muted-symbolic")
|
||||
} else {
|
||||
widget::icon::from_name("audio-volume-high-symbolic")
|
||||
})
|
||||
.on_press(Message::SinkMuteToggle),
|
||||
)
|
||||
.push(widget::text::body(&page.sink_volume_text))
|
||||
.push(
|
||||
widget::slider(0..=150, page.sink_volume, Message::SinkVolumeChanged)
|
||||
.width(250)
|
||||
.breakpoints(&[100]),
|
||||
);
|
||||
|
||||
let devices = widget::dropdown(
|
||||
&page.sinks,
|
||||
Some(page.active_sink.unwrap_or(0)),
|
||||
Message::SinkChanged,
|
||||
);
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&*section.descriptions[volume], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[device], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[level], text("TODO")))
|
||||
.add(settings::item(&*section.descriptions[config], text("TODO")))
|
||||
.into()
|
||||
.add(settings::item(
|
||||
&*section.descriptions[volume],
|
||||
volume_control,
|
||||
))
|
||||
.add(settings::item(&*section.descriptions[device], devices))
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Sound)
|
||||
})
|
||||
}
|
||||
|
||||
// fn alerts() -> Section<crate::pages::Message> {
|
||||
// let mut descriptions = Slab::new();
|
||||
// let volume = descriptions.insert(fl!("sound-alerts", "volume"));
|
||||
// let sound = descriptions.insert(fl!("sound-alerts", "sound"));
|
||||
|
||||
// Section::default()
|
||||
// .title(fl!("sound-alerts"))
|
||||
// .descriptions(descriptions)
|
||||
// .view::<Page>(move |_binder, _page, section| {
|
||||
// settings::view_section(§ion.title)
|
||||
// .add(settings::item(§ion.descriptions[volume], text("TODO")))
|
||||
// .add(settings::item(§ion.descriptions[sound], text("TODO")))
|
||||
// .into()
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn applications() -> Section<crate::pages::Message> {
|
||||
// let mut descriptions = Slab::new();
|
||||
|
||||
// let applications = descriptions.insert(fl!("sound-applications", "desc"));
|
||||
|
||||
// Section::default()
|
||||
// .title(fl!("sound-applications"))
|
||||
// .descriptions(descriptions)
|
||||
// .view::<Page>(move |_binder, _page, section| {
|
||||
// settings::view_section(§ion.title)
|
||||
// .add(settings::item(
|
||||
// &*section.descriptions[applications],
|
||||
// text("TODO"),
|
||||
// ))
|
||||
// .into()
|
||||
// })
|
||||
// }
|
||||
|
||||
fn wpctl_set_default(id: u32) {
|
||||
tokio::task::spawn(async move {
|
||||
let default = id.to_string();
|
||||
_ = tokio::process::Command::new("wpctl")
|
||||
.args(&["set-default", default.as_str()])
|
||||
.status()
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
fn wpctl_set_volume(id: u32, volume: u32) {
|
||||
tokio::task::spawn(async move {
|
||||
let id = id.to_string();
|
||||
let volume = format!("{}.{:02}", volume / 100, volume % 100);
|
||||
_ = tokio::process::Command::new("wpctl")
|
||||
.args(&["set-volume", id.as_str(), volume.as_str()])
|
||||
.status()
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
fn wpctl_set_mute(id: u32, mute: bool) {
|
||||
tokio::task::spawn(async move {
|
||||
let default = id.to_string();
|
||||
_ = tokio::process::Command::new("wpctl")
|
||||
.args(&["set-mute", default.as_str(), if mute { "1" } else { "0" }])
|
||||
.status()
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
3
debian/control
vendored
3
debian/control
vendored
|
|
@ -5,11 +5,12 @@ Maintainer: Michael Murphy <mmstick@pm.me>
|
|||
Build-Depends:
|
||||
debhelper-compat (=13),
|
||||
cmake,
|
||||
just (>= 1.13.0),
|
||||
just,
|
||||
libexpat1-dev,
|
||||
libfontconfig-dev,
|
||||
libfreetype-dev,
|
||||
libinput-dev,
|
||||
libpulse-dev,
|
||||
libudev-dev,
|
||||
libwayland-dev,
|
||||
libxkbcommon-dev,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue