wip
This commit is contained in:
parent
98dfd3acb9
commit
47f46ba9cd
4 changed files with 92 additions and 667 deletions
|
|
@ -11,7 +11,7 @@ cosmic-randr = { git = "https://github.com/pop-os/cosmic-randr" }
|
|||
tokio = { version = "1.47.0", features = ["macros"] }
|
||||
|
||||
[workspace.dependencies.libcosmic]
|
||||
features = ["multi-window", "winit", "tokio"]
|
||||
features = ["dbus-config", "multi-window", "winit", "tokio"]
|
||||
git = "https://github.com/pop-os/libcosmic"
|
||||
|
||||
[workspace.dependencies.cosmic-config]
|
||||
|
|
@ -33,7 +33,8 @@ git = "https://github.com/pop-os/cosmic-panel"
|
|||
git = "https://github.com/pop-os/cosmic-randr"
|
||||
|
||||
[workspace.dependencies.cosmic-settings-subscriptions]
|
||||
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
|
||||
# git = "https://github.com/pop-os/cosmic-settings-subscriptions"
|
||||
path = "../../pop/cosmic-settings-subscriptions"
|
||||
|
||||
[workspace.dependencies.sctk]
|
||||
git = "https://github.com/smithay/client-toolkit/"
|
||||
|
|
|
|||
|
|
@ -162,10 +162,7 @@ page-networking = [
|
|||
]
|
||||
page-power = ["dep:upower_dbus", "dep:zbus"]
|
||||
page-region = ["gettext", "dep:locales-rs", "dep:locale1", "dep:zbus"]
|
||||
page-sound = [
|
||||
"cosmic-settings-subscriptions/pipewire",
|
||||
"cosmic-settings-subscriptions/pulse",
|
||||
]
|
||||
page-sound = ["cosmic-settings-subscriptions/sound"]
|
||||
page-users = ["xdg-portal", "dep:accounts-zbus", "dep:zbus", "dep:zbus_polkit"]
|
||||
page-window-management = ["dep:cosmic-settings-config"]
|
||||
page-workspaces = ["dep:cosmic-comp-config"]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{collections::BTreeMap, sync::Arc, time::Duration};
|
||||
|
||||
use cosmic::{
|
||||
Apply, Element, Task,
|
||||
iced::{Alignment, Length, window},
|
||||
|
|
@ -10,66 +8,36 @@ use cosmic::{
|
|||
widget::{self, settings},
|
||||
};
|
||||
use cosmic_settings_page::{self as page, Section, section};
|
||||
use cosmic_settings_subscriptions::{pipewire, pulse};
|
||||
use futures::StreamExt;
|
||||
use indexmap::IndexMap;
|
||||
use slab::Slab;
|
||||
use slotmap::SlotMap;
|
||||
|
||||
pub type NodeId = u32;
|
||||
pub type ProfileId = u32;
|
||||
use cosmic_settings_subscriptions::sound as subscription;
|
||||
|
||||
#[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),
|
||||
SinkBalanceChanged(u32),
|
||||
/// Change the default output.
|
||||
SinkChanged(usize),
|
||||
/// Change the active profile for an output.
|
||||
SinkProfileChanged(usize),
|
||||
/// Select a device from the given card after a profile change.
|
||||
SinkProfileSelect(DeviceId),
|
||||
/// Request to change the default output volume.
|
||||
SinkVolumeChanged(u32),
|
||||
/// Request to change the default output balance.
|
||||
SinkBalanceChanged(u32),
|
||||
/// Change the output volume.
|
||||
SinkVolumeApply(NodeId),
|
||||
/// Change the output balance.
|
||||
SinkBalanceApply,
|
||||
/// 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),
|
||||
/// Change the default input output.
|
||||
SourceChanged(usize),
|
||||
/// Change the active profile for an output.
|
||||
SourceProfileChanged(usize),
|
||||
/// Select a device from the given card after a profile change.
|
||||
SourceProfileSelect(DeviceId),
|
||||
/// Request to change the input volume.
|
||||
SourceVolumeChanged(u32),
|
||||
/// Change the input volume.
|
||||
SourceVolumeApply(NodeId),
|
||||
/// Toggle the mute status of the input output.
|
||||
SourceMuteToggle,
|
||||
/// On init of the subscription, channels for closing background threads are given to the app.
|
||||
SubHandle(Arc<SubscriptionHandle>),
|
||||
/// Change the active profile for an output.
|
||||
SourceProfileChanged(usize),
|
||||
/// Request to change the input volume.
|
||||
SourceVolumeChanged(u32),
|
||||
///
|
||||
Subscription(subscription::sound::Message),
|
||||
/// Surface Action
|
||||
Surface(surface::Action),
|
||||
}
|
||||
|
||||
pub struct SubscriptionHandle {
|
||||
cancel_tx: futures::channel::oneshot::Sender<()>,
|
||||
pipewire: pipewire::Sender<()>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SubscriptionHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("SubscriptionHandle")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for crate::pages::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::Sound(message)
|
||||
|
|
@ -82,91 +50,16 @@ impl From<Message> for crate::Message {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Card {
|
||||
devices: IndexMap<NodeId, Device>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Device {
|
||||
class: pipewire::MediaClass,
|
||||
identifier: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||
pub enum DeviceId {
|
||||
Alsa(u32),
|
||||
Bluez5(String),
|
||||
Unknown(),
|
||||
impl Into<Message> for subscription::sound::Message {
|
||||
fn into(self) -> Message {
|
||||
Message::Subscription(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page {
|
||||
entity: page::Entity,
|
||||
subscription_handle: Option<SubscriptionHandle>,
|
||||
sink_channels: Option<pulse::PulseChannels>,
|
||||
|
||||
devices: BTreeMap<DeviceId, Card>,
|
||||
card_names: IndexMap<DeviceId, String>,
|
||||
card_profiles: IndexMap<DeviceId, Vec<pulse::CardProfile>>,
|
||||
active_profiles: IndexMap<DeviceId, Option<String>>,
|
||||
|
||||
/** Sink devices */
|
||||
|
||||
/// Product names for source sink devices.
|
||||
sinks: Vec<String>,
|
||||
/// Pipewire object IDs for sink devices.
|
||||
sink_pw_ids: Vec<NodeId>,
|
||||
/// Profile IDs for the actively-selected sink device.
|
||||
sink_profiles: Vec<String>,
|
||||
/// Names of profiles for the actively-selected sink device.
|
||||
sink_profile_names: Vec<String>,
|
||||
/// Device ID of active sink device.
|
||||
active_sink_device: Option<DeviceId>,
|
||||
/// Index of active sink device.
|
||||
active_sink: Option<usize>,
|
||||
/// Card profile index of active sink device.
|
||||
active_sink_profile: Option<usize>,
|
||||
|
||||
/** Source devices */
|
||||
|
||||
/// Product names for source devices.
|
||||
sources: Vec<String>,
|
||||
/// Pipewire object IDs for source devices.
|
||||
source_pw_ids: Vec<NodeId>,
|
||||
/// Profile IDs for the actively-selected source device.
|
||||
source_profiles: Vec<String>,
|
||||
/// Names of profiles for the actively-selected source device.
|
||||
source_profile_names: Vec<String>,
|
||||
/// Device ID of active source device.
|
||||
active_source_device: Option<DeviceId>,
|
||||
/// Index of active source device.
|
||||
active_source: Option<usize>,
|
||||
/// Card profile index of active source device.
|
||||
active_source_profile: Option<usize>,
|
||||
|
||||
/// Device identifier of the default sink.
|
||||
default_sink: String,
|
||||
/// Device identifier of the default source.
|
||||
default_source: String,
|
||||
|
||||
sink_volume_text: String,
|
||||
source_volume_text: String,
|
||||
|
||||
sink_balance_text: Option<String>,
|
||||
sink_balance: Option<f32>,
|
||||
|
||||
sink_volume: u32,
|
||||
source_volume: u32,
|
||||
|
||||
sink_mute: bool,
|
||||
sink_volume_debounce: bool,
|
||||
sink_balance_debounce: bool,
|
||||
source_mute: bool,
|
||||
source_volume_debounce: bool,
|
||||
changing_sink_profile: bool,
|
||||
changing_source_profile: bool,
|
||||
model: subscription::sound::Model,
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
|
|
@ -187,71 +80,12 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
&self,
|
||||
_core: &cosmic::Core,
|
||||
) -> cosmic::iced::Subscription<crate::pages::Message> {
|
||||
cosmic::iced::Subscription::run(|| {
|
||||
async_fn_stream::fn_stream(|emitter| async move {
|
||||
let (cancel_tx, mut cancel_rx) = futures::channel::oneshot::channel::<()>();
|
||||
|
||||
let (tx, mut pulse_rx) = futures::channel::mpsc::channel(1);
|
||||
let _pulse_handle = std::thread::spawn(move || {
|
||||
pulse::thread(tx);
|
||||
});
|
||||
|
||||
let (tx, mut pw_rx) = futures::channel::mpsc::channel(1);
|
||||
let (_pipewire_handle, pipewire_terminate) = pipewire::thread(tx);
|
||||
|
||||
emitter
|
||||
.emit(
|
||||
Message::SubHandle(Arc::new(SubscriptionHandle {
|
||||
cancel_tx,
|
||||
pipewire: pipewire_terminate,
|
||||
}))
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
loop {
|
||||
futures::select! {
|
||||
event = pulse_rx.next() => {
|
||||
let Some(event) = event else {
|
||||
break;
|
||||
};
|
||||
|
||||
emitter
|
||||
.emit(crate::pages::Message::from(Message::Pulse(event)))
|
||||
.await;
|
||||
}
|
||||
|
||||
event = pw_rx.next() => {
|
||||
let Some(event) = event else {
|
||||
break;
|
||||
};
|
||||
|
||||
emitter
|
||||
.emit(crate::pages::Message::from(Message::Pipewire(event)))
|
||||
.await;
|
||||
}
|
||||
|
||||
_ = cancel_rx => break,
|
||||
}
|
||||
}
|
||||
|
||||
drop(pulse_rx);
|
||||
drop(pw_rx);
|
||||
|
||||
futures::future::pending::<crate::pages::Message>().await;
|
||||
})
|
||||
})
|
||||
cosmic::iced::Subscription::run(|| subscription::watch())
|
||||
.map(|message| Message::Subscription(message).into())
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> Task<crate::pages::Message> {
|
||||
if let Some(handle) = self.subscription_handle.take() {
|
||||
_ = handle.cancel_tx.send(());
|
||||
_ = handle.pipewire.send(());
|
||||
}
|
||||
|
||||
if let Some(channel) = self.sink_channels.take() {
|
||||
channel.quit();
|
||||
}
|
||||
self.model.clear();
|
||||
|
||||
*self = Page {
|
||||
entity: self.entity,
|
||||
|
|
@ -265,421 +99,70 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl Page {
|
||||
fn device_profiles(&self, device_id: &DeviceId) -> (Vec<String>, Vec<String>, Option<usize>) {
|
||||
let (profiles, profile_descriptions): (Vec<String>, Vec<String>) = self
|
||||
.card_profiles
|
||||
.get(device_id)
|
||||
.map_or((Vec::new(), Vec::new()), |profiles| {
|
||||
profiles
|
||||
.iter()
|
||||
.filter(|p| p.available && p.name != "off")
|
||||
.map(|p| (p.name.clone(), p.description.clone()))
|
||||
.collect()
|
||||
});
|
||||
|
||||
let active_profile = self.active_profiles.get(device_id).and_then(|profile| {
|
||||
profile
|
||||
.as_ref()
|
||||
.and_then(|profile| profiles.iter().position(|p| p == profile))
|
||||
});
|
||||
|
||||
(profiles, profile_descriptions, active_profile)
|
||||
}
|
||||
|
||||
fn set_sink_profiles(&mut self, device_id: &DeviceId) {
|
||||
(
|
||||
self.sink_profile_names,
|
||||
self.sink_profiles,
|
||||
self.active_sink_profile,
|
||||
) = self.device_profiles(device_id);
|
||||
}
|
||||
|
||||
fn set_source_profiles(&mut self, device_id: &DeviceId) {
|
||||
(
|
||||
self.source_profile_names,
|
||||
self.source_profiles,
|
||||
self.active_source_profile,
|
||||
) = self.device_profiles(device_id);
|
||||
}
|
||||
|
||||
fn set_default_sink(&mut self, sink: String) {
|
||||
if self.default_sink == sink {
|
||||
return;
|
||||
}
|
||||
|
||||
self.default_sink = sink;
|
||||
self.active_sink_profile = None;
|
||||
self.sink_profiles.clear();
|
||||
self.sink_profile_names.clear();
|
||||
|
||||
for (device_id, card) in &self.devices {
|
||||
for (&node_id, device) in &card.devices {
|
||||
if let pipewire::MediaClass::Sink = device.class {
|
||||
if device.identifier == self.default_sink {
|
||||
self.active_sink = self.sink_pw_ids.iter().position(|&id| id == node_id);
|
||||
let device_id = device_id.clone();
|
||||
self.set_sink_profiles(&device_id);
|
||||
self.active_sink_device = Some(device_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_default_source(&mut self, source: String) {
|
||||
if self.default_source == source {
|
||||
return;
|
||||
}
|
||||
|
||||
self.default_source = source;
|
||||
self.active_source_profile = None;
|
||||
self.source_profiles.clear();
|
||||
self.source_profile_names.clear();
|
||||
|
||||
for (device_id, card) in &self.devices {
|
||||
for (&node_id, device) in &card.devices {
|
||||
if let pipewire::MediaClass::Source = device.class {
|
||||
if device.identifier == self.default_source {
|
||||
self.active_source =
|
||||
self.source_pw_ids.iter().position(|&id| id == node_id);
|
||||
let device_id = device_id.clone();
|
||||
self.set_source_profiles(&device_id);
|
||||
self.active_source_device = Some(device_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Task<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 Task::none();
|
||||
}
|
||||
|
||||
let mut command = None;
|
||||
if let Some(&node_id) = self.source_pw_ids.get(self.active_source.unwrap_or(0)) {
|
||||
command = Some(cosmic::Task::future(async move {
|
||||
tokio::time::sleep(Duration::from_millis(64)).await;
|
||||
Message::SourceVolumeApply(node_id).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 Task::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 Task::none();
|
||||
}
|
||||
|
||||
let mut command = None;
|
||||
if let Some(&node_id) = self.sink_pw_ids.get(self.active_sink.unwrap_or(0)) {
|
||||
command = Some(cosmic::Task::future(async move {
|
||||
tokio::time::sleep(Duration::from_millis(64)).await;
|
||||
Message::SinkVolumeApply(node_id).into()
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(command) = command {
|
||||
self.sink_volume_debounce = true;
|
||||
return command;
|
||||
}
|
||||
}
|
||||
Message::SinkBalanceChanged(balance) => {
|
||||
self.sink_balance = Some((balance as f32 - 100.) / 100.);
|
||||
self.sink_balance_text = Some(format!("{balance:.2}"));
|
||||
if self.sink_balance_debounce {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
let mut command = None;
|
||||
if !self
|
||||
.sink_pw_ids
|
||||
.get(self.active_sink.unwrap_or(0))
|
||||
.is_none()
|
||||
{
|
||||
command = Some(cosmic::Task::future(async move {
|
||||
tokio::time::sleep(Duration::from_millis(64)).await;
|
||||
Message::SinkBalanceApply.into()
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(command) = command {
|
||||
self.sink_balance_debounce = true;
|
||||
return command;
|
||||
}
|
||||
}
|
||||
Message::Pulse(pulse::Event::SinkVolume(volume)) => {
|
||||
if self.sink_volume_debounce {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
self.sink_volume = volume;
|
||||
self.sink_volume_text = volume.to_string();
|
||||
}
|
||||
Message::Pulse(pulse::Event::DefaultSink(sink)) => {
|
||||
if !self.changing_sink_profile {
|
||||
self.set_default_sink(sink);
|
||||
}
|
||||
}
|
||||
Message::Pulse(pulse::Event::DefaultSource(source)) => {
|
||||
if !self.changing_source_profile {
|
||||
self.set_default_source(source);
|
||||
}
|
||||
}
|
||||
Message::Pulse(pulse::Event::SinkMute(mute)) => {
|
||||
self.sink_mute = mute;
|
||||
}
|
||||
Message::Pulse(pulse::Event::SourceMute(mute)) => {
|
||||
self.source_mute = mute;
|
||||
}
|
||||
Message::Pulse(pulse::Event::CardInfo(card)) => {
|
||||
let device_id = match card.variant {
|
||||
pulse::DeviceVariant::Alsa { alsa_card, .. } => DeviceId::Alsa(alsa_card),
|
||||
pulse::DeviceVariant::Bluez5 { address, .. } => DeviceId::Bluez5(address),
|
||||
};
|
||||
|
||||
self.card_names.insert(device_id.clone(), card.name);
|
||||
self.card_profiles.insert(device_id.clone(), card.profiles);
|
||||
self.active_profiles
|
||||
.insert(device_id, card.active_profile.map(|p| p.name));
|
||||
}
|
||||
Message::Pulse(pulse::Event::Balance(balance)) => {
|
||||
self.sink_balance = balance;
|
||||
self.sink_balance_text = balance.map(|b| format!("{b:.2}"));
|
||||
}
|
||||
Message::Pulse(pulse::Event::Channels(channels)) => {
|
||||
self.sink_channels = Some(channels);
|
||||
}
|
||||
Message::Pipewire(pipewire::DeviceEvent::Add(device)) => {
|
||||
let device_id = match device.variant {
|
||||
pipewire::DeviceVariant::Alsa { alsa_card, .. } => DeviceId::Alsa(alsa_card),
|
||||
pipewire::DeviceVariant::Bluez5 { address, .. } => DeviceId::Bluez5(address),
|
||||
pipewire::DeviceVariant::Unknown {} => DeviceId::Unknown {},
|
||||
};
|
||||
|
||||
match device.media_class {
|
||||
pipewire::MediaClass::Sink => {
|
||||
self.sinks.push(device.product_name.clone());
|
||||
self.sink_pw_ids.push(device.object_id);
|
||||
|
||||
sort_pulse_devices(&mut self.sinks, &mut self.sink_pw_ids);
|
||||
|
||||
if self.default_sink == device.node_name {
|
||||
self.active_sink =
|
||||
self.sinks.iter().position(|s| *s == device.product_name);
|
||||
self.active_sink_device = Some(device_id.clone());
|
||||
self.set_sink_profiles(&device_id);
|
||||
}
|
||||
}
|
||||
|
||||
pipewire::MediaClass::Source => {
|
||||
self.sources.push(device.product_name.clone());
|
||||
self.source_pw_ids.push(device.object_id);
|
||||
|
||||
sort_pulse_devices(&mut self.sources, &mut self.source_pw_ids);
|
||||
|
||||
if self.default_source == device.node_name {
|
||||
self.active_source =
|
||||
self.sources.iter().position(|s| *s == device.product_name);
|
||||
self.active_source_device = Some(device_id.clone());
|
||||
self.set_source_profiles(&device_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let card = self.devices.entry(device_id).or_insert_with(|| Card {
|
||||
devices: IndexMap::new(),
|
||||
});
|
||||
|
||||
card.devices.insert(
|
||||
device.object_id,
|
||||
Device {
|
||||
class: device.media_class,
|
||||
identifier: device.node_name,
|
||||
description: device.product_name,
|
||||
},
|
||||
);
|
||||
|
||||
card.devices
|
||||
.sort_unstable_by(|_, av, _, bv| av.description.cmp(&bv.description));
|
||||
}
|
||||
Message::Pipewire(pipewire::DeviceEvent::Remove(node_id)) => {
|
||||
let mut remove = None;
|
||||
for (card_id, card) in &mut self.devices {
|
||||
if card.devices.shift_remove(&node_id).is_some() {
|
||||
if card.devices.is_empty() {
|
||||
remove = Some(card_id.clone());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(card_id) = remove {
|
||||
_ = self.devices.remove(&card_id);
|
||||
}
|
||||
|
||||
if let Some(pos) = self.sink_pw_ids.iter().position(|&id| id == node_id) {
|
||||
_ = self.sink_pw_ids.remove(pos);
|
||||
_ = self.sinks.remove(pos);
|
||||
if self.active_sink == Some(pos) {
|
||||
self.active_sink = None;
|
||||
self.active_sink_device = None;
|
||||
self.active_sink_profile = None;
|
||||
}
|
||||
} else if let Some(pos) = self.source_pw_ids.iter().position(|&id| id == node_id) {
|
||||
_ = self.source_pw_ids.remove(pos);
|
||||
_ = self.sources.remove(pos);
|
||||
if self.active_source == Some(pos) {
|
||||
self.active_source = None;
|
||||
self.active_source_device = None;
|
||||
self.active_source_profile = None;
|
||||
}
|
||||
}
|
||||
return self
|
||||
.model
|
||||
.sink_balance_changed(balance)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
Message::SinkChanged(pos) => {
|
||||
if let Some(node_id) = self.sink_pw_ids.get(pos) {
|
||||
for card in self.devices.values() {
|
||||
for (nid, device) in &card.devices {
|
||||
if node_id == nid {
|
||||
self.active_sink = Some(pos);
|
||||
pactl_set_default_sink(device.identifier.clone());
|
||||
self.set_default_sink(device.identifier.clone());
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SourceChanged(pos) => {
|
||||
if let Some(node_id) = self.source_pw_ids.get(pos) {
|
||||
for card in self.devices.values() {
|
||||
for (nid, device) in &card.devices {
|
||||
if node_id == nid {
|
||||
self.active_source = Some(pos);
|
||||
pactl_set_default_source(device.identifier.clone());
|
||||
self.set_default_source(device.identifier.clone());
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SinkBalanceApply => {
|
||||
self.sink_balance_debounce = false;
|
||||
if let Some((balance, channels)) =
|
||||
self.sink_balance.zip(self.sink_channels.as_mut())
|
||||
{
|
||||
channels.set_balance(balance);
|
||||
}
|
||||
}
|
||||
Message::SinkVolumeApply(_) => {
|
||||
self.sink_volume_debounce = false;
|
||||
if let Some(channels) = self.sink_channels.as_mut() {
|
||||
channels.set_volume(self.sink_volume as f32 / 100.);
|
||||
}
|
||||
}
|
||||
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_pw_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_pw_ids.get(self.active_source.unwrap_or(0)) {
|
||||
wpctl_set_mute(node_id, self.source_mute);
|
||||
}
|
||||
return self
|
||||
.model
|
||||
.sink_changed(pos)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::SinkMuteToggle => self.model.sink_mute_toggle(),
|
||||
|
||||
Message::SinkProfileChanged(profile) => {
|
||||
self.active_sink_profile = Some(profile);
|
||||
|
||||
if let Some(profile) = self.sink_profile_names.get(profile).cloned() {
|
||||
if let Some(device_id) = self.active_sink_device.clone() {
|
||||
if let Some(name) = self.card_names.get(&device_id).cloned() {
|
||||
self.active_profiles
|
||||
.insert(device_id.clone(), Some(profile.clone()));
|
||||
|
||||
self.changing_sink_profile = true;
|
||||
return cosmic::Task::future(async move {
|
||||
pactl_set_card_profile(name, profile).await;
|
||||
Message::SinkProfileSelect(device_id).into()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return self
|
||||
.model
|
||||
.sink_profile_changed(profile)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
Message::SinkProfileSelect(device_id) => {
|
||||
self.changing_sink_profile = false;
|
||||
let sink_pos = self.active_sink.unwrap_or(0);
|
||||
|
||||
if let Some(card) = self.devices.get(&device_id) {
|
||||
if let Some((_, device)) = card.devices.get_index(sink_pos) {
|
||||
pactl_set_default_sink(device.identifier.clone());
|
||||
self.set_default_sink(device.identifier.clone());
|
||||
}
|
||||
}
|
||||
Message::SinkVolumeChanged(volume) => {
|
||||
return self
|
||||
.model
|
||||
.sink_volume_changed(volume)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
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) => {
|
||||
self.active_source_profile = Some(profile);
|
||||
if let Some(profile) = self.source_profile_names.get(profile).cloned() {
|
||||
if let Some(device_id) = self.active_source_device.clone() {
|
||||
if let Some(name) = self.card_names.get(&device_id).cloned() {
|
||||
self.active_profiles
|
||||
.insert(device_id.clone(), Some(profile.clone()));
|
||||
|
||||
self.changing_source_profile = true;
|
||||
return cosmic::Task::future(async move {
|
||||
pactl_set_card_profile(name, profile).await;
|
||||
Message::SourceProfileSelect(device_id).into()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SourceProfileSelect(device_id) => {
|
||||
self.changing_source_profile = false;
|
||||
let source_pos = self.active_source.unwrap_or(0);
|
||||
|
||||
if let Some(card) = self.devices.get(&device_id) {
|
||||
if let Some((_, device)) = card.devices.get_index(source_pos) {
|
||||
pactl_set_default_source(device.identifier.clone());
|
||||
self.set_default_source(device.identifier.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Surface(a) => {
|
||||
return cosmic::task::message(crate::app::Message::Surface(a));
|
||||
return self
|
||||
.model
|
||||
.source_profile_changed(profile)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::SubHandle(handle) => {
|
||||
if let Some(handle) = Arc::into_inner(handle) {
|
||||
self.subscription_handle = Some(handle);
|
||||
}
|
||||
Message::SourceVolumeChanged(volume) => {
|
||||
return self
|
||||
.model
|
||||
.source_volume_changed(volume)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::Subscription(message) => {
|
||||
return self
|
||||
.model
|
||||
.update(message)
|
||||
.map(|message| Message::Subscription(message).into());
|
||||
}
|
||||
|
||||
Message::Surface(a) => return cosmic::task::message(crate::app::Message::Surface(a)),
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
|
@ -696,14 +179,14 @@ fn input() -> Section<crate::pages::Message> {
|
|||
.title(fl!("sound-input"))
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
if page.sources.is_empty() {
|
||||
if page.model.sources().is_empty() {
|
||||
return widget::row().into();
|
||||
}
|
||||
|
||||
let volume_control = widget::row::with_capacity(4)
|
||||
.align_y(Alignment::Center)
|
||||
.push(
|
||||
widget::button::icon(widget::icon::from_name(if page.source_mute {
|
||||
widget::button::icon(widget::icon::from_name(if page.model.source_mute {
|
||||
"microphone-sensitivity-muted-symbolic"
|
||||
} else {
|
||||
"audio-input-microphone-symbolic"
|
||||
|
|
@ -711,20 +194,20 @@ fn input() -> Section<crate::pages::Message> {
|
|||
.on_press(Message::SourceMuteToggle.into()),
|
||||
)
|
||||
.push(
|
||||
widget::text::body(&page.source_volume_text)
|
||||
widget::text::body(&page.model.source_volume_text)
|
||||
.width(Length::Fixed(22.0))
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
.push(widget::horizontal_space().width(8))
|
||||
.push(
|
||||
widget::slider(0..=150, page.source_volume, |change| {
|
||||
widget::slider(0..=150, page.model.source_volume, |change| {
|
||||
Message::SourceVolumeChanged(change).into()
|
||||
})
|
||||
.breakpoints(&[100]),
|
||||
);
|
||||
let devices = widget::dropdown::popup_dropdown(
|
||||
&page.sources,
|
||||
Some(page.active_source.unwrap_or(0)),
|
||||
page.model.sources(),
|
||||
Some(page.model.active_source().unwrap_or(0)),
|
||||
Message::SourceChanged,
|
||||
window::Id::RESERVED,
|
||||
Message::Surface,
|
||||
|
|
@ -741,10 +224,10 @@ fn input() -> Section<crate::pages::Message> {
|
|||
))
|
||||
.add(settings::item(&*section.descriptions[device], devices));
|
||||
|
||||
if !page.source_profiles.is_empty() {
|
||||
if !page.model.source_profiles().is_empty() {
|
||||
let dropdown = widget::dropdown::popup_dropdown(
|
||||
&page.source_profiles,
|
||||
page.active_source_profile,
|
||||
page.model.source_profiles(),
|
||||
page.model.active_source_profile(),
|
||||
Message::SourceProfileChanged,
|
||||
window::Id::RESERVED,
|
||||
Message::Surface,
|
||||
|
|
@ -779,7 +262,7 @@ fn output() -> Section<crate::pages::Message> {
|
|||
let volume_control = widget::row::with_capacity(4)
|
||||
.align_y(Alignment::Center)
|
||||
.push(
|
||||
widget::button::icon(if page.sink_mute {
|
||||
widget::button::icon(if page.model.sink_mute {
|
||||
widget::icon::from_name("audio-volume-muted-symbolic")
|
||||
} else {
|
||||
widget::icon::from_name("audio-volume-high-symbolic")
|
||||
|
|
@ -787,21 +270,21 @@ fn output() -> Section<crate::pages::Message> {
|
|||
.on_press(Message::SinkMuteToggle.into()),
|
||||
)
|
||||
.push(
|
||||
widget::text::body(&page.sink_volume_text)
|
||||
widget::text::body(&page.model.sink_volume_text)
|
||||
.width(Length::Fixed(22.0))
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
.push(widget::horizontal_space().width(8))
|
||||
.push(
|
||||
widget::slider(0..=150, page.sink_volume, |change| {
|
||||
widget::slider(0..=150, page.model.sink_volume, |change| {
|
||||
Message::SinkVolumeChanged(change).into()
|
||||
})
|
||||
.breakpoints(&[100]),
|
||||
);
|
||||
|
||||
let devices = widget::dropdown::popup_dropdown(
|
||||
&page.sinks,
|
||||
Some(page.active_sink.unwrap_or(0)),
|
||||
page.model.sinks(),
|
||||
Some(page.model.active_sink().unwrap_or(0)),
|
||||
Message::SinkChanged,
|
||||
window::Id::RESERVED,
|
||||
Message::Surface,
|
||||
|
|
@ -818,10 +301,10 @@ fn output() -> Section<crate::pages::Message> {
|
|||
))
|
||||
.add(settings::item(&*section.descriptions[device], devices));
|
||||
|
||||
if !page.sink_profiles.is_empty() {
|
||||
if !page.model.sink_profiles().is_empty() {
|
||||
let dropdown = widget::dropdown::popup_dropdown(
|
||||
&page.sink_profiles,
|
||||
page.active_sink_profile,
|
||||
page.model.sink_profiles(),
|
||||
page.model.active_sink_profile(),
|
||||
Message::SinkProfileChanged,
|
||||
window::Id::RESERVED,
|
||||
Message::Surface,
|
||||
|
|
@ -832,7 +315,7 @@ fn output() -> Section<crate::pages::Message> {
|
|||
|
||||
controls = controls.add(settings::item(&*section.descriptions[profile], dropdown));
|
||||
}
|
||||
if let Some(sink_balance) = page.sink_balance {
|
||||
if let Some(sink_balance) = page.model.sink_balance {
|
||||
controls = controls.add(settings::item(
|
||||
&*section.descriptions[balance],
|
||||
widget::row::with_capacity(4)
|
||||
|
|
@ -897,60 +380,3 @@ fn output() -> Section<crate::pages::Message> {
|
|||
// .into()
|
||||
// })
|
||||
// }
|
||||
|
||||
fn sort_pulse_devices(descriptions: &mut Vec<String>, node_ids: &mut Vec<NodeId>) {
|
||||
let mut tmp: Vec<(String, NodeId)> = std::mem::take(descriptions)
|
||||
.into_iter()
|
||||
.zip(std::mem::take(node_ids))
|
||||
.collect();
|
||||
|
||||
tmp.sort_unstable_by(|(ak, _), (bk, _)| ak.cmp(bk));
|
||||
|
||||
(*descriptions, *node_ids) = tmp.into_iter().collect();
|
||||
}
|
||||
|
||||
async fn pactl_set_card_profile(id: String, profile: String) {
|
||||
_ = tokio::process::Command::new("pactl")
|
||||
.args(["set-card-profile", id.as_str(), profile.as_str()])
|
||||
.status()
|
||||
.await
|
||||
}
|
||||
|
||||
fn pactl_set_default_sink(id: String) {
|
||||
tokio::task::spawn(async move {
|
||||
_ = tokio::process::Command::new("pactl")
|
||||
.args(["set-default-sink", id.as_str()])
|
||||
.status()
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
fn pactl_set_default_source(id: String) {
|
||||
tokio::task::spawn(async move {
|
||||
_ = tokio::process::Command::new("pactl")
|
||||
.args(["set-default-source", id.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;
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::iced_core::text::Wrapping;
|
||||
use cosmic_settings_page::{self as page, Section, section};
|
||||
|
||||
use cosmic::widget::{editable_input, list_column, settings, text};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue