feat(sound): sound profiles, bluetooth codecs, and fixes
This commit is contained in:
parent
6f26ad7974
commit
c63ae64029
7 changed files with 211 additions and 81 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1621,7 +1621,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-settings-subscriptions"
|
name = "cosmic-settings-subscriptions"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/pop-os/cosmic-settings-subscriptions#9634f708d079f4b6336e02ed3e22ede6ee7f7cdb"
|
source = "git+https://github.com/pop-os/cosmic-settings-subscriptions#f6fa655e4b74a5bd2dbfc2f6fdd94bc78f5e4fcc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"iced_futures",
|
"iced_futures",
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ slab = "0.4.9"
|
||||||
|
|
||||||
[dependencies.cosmic-settings-subscriptions]
|
[dependencies.cosmic-settings-subscriptions]
|
||||||
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
|
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
|
||||||
|
# path = "../../cosmic-settings-subscriptions"
|
||||||
features = ["pipewire", "pulse"]
|
features = ["pipewire", "pulse"]
|
||||||
|
|
||||||
[dependencies.icu]
|
[dependencies.icu]
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ impl Default for Page {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let comp_config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
|
let comp_config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
|
||||||
let comp_workspace_config = comp_config.get("workspaces").unwrap_or_else(|err| {
|
let comp_workspace_config = comp_config.get("workspaces").unwrap_or_else(|err| {
|
||||||
if !matches!(cosmic_config::Error::NoConfigDirectory, err) {
|
if !matches!(err, cosmic_config::Error::NoConfigDirectory) {
|
||||||
error!(?err, "Failed to read config 'workspaces'");
|
error!(?err, "Failed to read config 'workspaces'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,14 +44,14 @@ impl Default for Page {
|
||||||
});
|
});
|
||||||
let config = cosmic_config::Config::new("com.system76.CosmicWorkspaces", 1).unwrap();
|
let config = cosmic_config::Config::new("com.system76.CosmicWorkspaces", 1).unwrap();
|
||||||
let show_workspace_name = config.get("show_workspace_name").unwrap_or_else(|err| {
|
let show_workspace_name = config.get("show_workspace_name").unwrap_or_else(|err| {
|
||||||
if !matches!(cosmic_config::Error::NoConfigDirectory, err) {
|
if !matches!(err, cosmic_config::Error::NoConfigDirectory) {
|
||||||
error!(?err, "Failed to read config 'show_workspace_name'");
|
error!(?err, "Failed to read config 'show_workspace_name'");
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
});
|
});
|
||||||
let show_workspace_number = config.get("show_workspace_number").unwrap_or_else(|err| {
|
let show_workspace_number = config.get("show_workspace_number").unwrap_or_else(|err| {
|
||||||
if !matches!(cosmic_config::Error::NoConfigDirectory, err) {
|
if !matches!(err, cosmic_config::Error::NoConfigDirectory) {
|
||||||
error!(?err, "Failed to read config 'show_workspace_number'");
|
error!(?err, "Failed to read config 'show_workspace_number'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ fn get_config<T: Default + serde::de::DeserializeOwned>(
|
||||||
key: &str,
|
key: &str,
|
||||||
) -> T {
|
) -> T {
|
||||||
config.get(key).unwrap_or_else(|why| {
|
config.get(key).unwrap_or_else(|why| {
|
||||||
if !matches!(cosmic_config::Error::NoConfigDirectory, why) {
|
if !matches!(why, cosmic_config::Error::NoConfigDirectory) {
|
||||||
error!(?why, "Failed to read config '{}'", key);
|
error!(?why, "Failed to read config '{}'", key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// Copyright 2023 System76 <info@system76.com>
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use std::{collections::HashMap, time::Duration};
|
use std::{collections::BTreeMap, time::Duration};
|
||||||
|
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
widget::{self, settings},
|
widget::{self, settings},
|
||||||
Apply, Command, Element,
|
Command, Element,
|
||||||
};
|
};
|
||||||
use cosmic_settings_page::{self as page, section, Section};
|
use cosmic_settings_page::{self as page, section, Section};
|
||||||
use cosmic_settings_subscriptions::{pipewire, pulse};
|
use cosmic_settings_subscriptions::{pipewire, pulse};
|
||||||
|
|
@ -19,39 +19,43 @@ pub enum Message {
|
||||||
Pulse(pulse::Event),
|
Pulse(pulse::Event),
|
||||||
/// Get ALSA cards and their profiles.
|
/// Get ALSA cards and their profiles.
|
||||||
Pipewire(pipewire::DeviceEvent),
|
Pipewire(pipewire::DeviceEvent),
|
||||||
/// Change the default microphone input.
|
/// Change the default output.
|
||||||
SinkChanged(usize),
|
SinkChanged(usize),
|
||||||
/// Request to change the default microphone volume.
|
/// Change the active profile for an output.
|
||||||
|
SinkProfileChanged(usize),
|
||||||
|
/// Request to change the default output volume.
|
||||||
SinkVolumeChanged(u32),
|
SinkVolumeChanged(u32),
|
||||||
/// Change the microphone volume.
|
/// Change the output volume.
|
||||||
SinkVolumeApply(NodeId),
|
SinkVolumeApply(NodeId),
|
||||||
/// Toggle the mute status of the microphone.
|
/// Toggle the mute status of the output.
|
||||||
SinkMuteToggle,
|
SinkMuteToggle,
|
||||||
/// Change the default speaker output.
|
/// Change the default input output.
|
||||||
SourceChanged(usize),
|
SourceChanged(usize),
|
||||||
/// Request to change the speaker volume.
|
/// Change the active profile for an output.
|
||||||
|
SourceProfileChanged(usize),
|
||||||
|
/// Request to change the input volume.
|
||||||
SourceVolumeChanged(u32),
|
SourceVolumeChanged(u32),
|
||||||
/// Change the speaker volume.
|
/// Change the input volume.
|
||||||
SourceVolumeApply(NodeId),
|
SourceVolumeApply(NodeId),
|
||||||
/// Toggle the mute status of the speaker output.
|
/// Toggle the mute status of the input output.
|
||||||
SourceMuteToggle,
|
SourceMuteToggle,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type NodeId = u32;
|
pub type NodeId = u32;
|
||||||
pub type ProfileId = u32;
|
pub type ProfileId = u32;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct Card {
|
struct Card {
|
||||||
class: pipewire::MediaClass,
|
devices: BTreeMap<NodeId, Port>,
|
||||||
// name: String,
|
|
||||||
profiles: HashMap<NodeId, Profile>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Profile {
|
#[derive(Debug)]
|
||||||
// device: ProfileId,
|
struct Port {
|
||||||
|
class: pipewire::MediaClass,
|
||||||
identifier: String,
|
identifier: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||||
enum DeviceId {
|
enum DeviceId {
|
||||||
Alsa(u32),
|
Alsa(u32),
|
||||||
Bluez5(String),
|
Bluez5(String),
|
||||||
|
|
@ -61,7 +65,12 @@ enum DeviceId {
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
pipewire_thread: Option<(tokio::sync::oneshot::Sender<()>, pipewire::Sender<()>)>,
|
pipewire_thread: Option<(tokio::sync::oneshot::Sender<()>, pipewire::Sender<()>)>,
|
||||||
pulse_thread: Option<tokio::sync::oneshot::Sender<()>>,
|
pulse_thread: Option<tokio::sync::oneshot::Sender<()>>,
|
||||||
devices: HashMap<DeviceId, Card>,
|
devices: BTreeMap<DeviceId, Card>,
|
||||||
|
card_names: BTreeMap<DeviceId, String>,
|
||||||
|
card_ports: BTreeMap<DeviceId, Vec<pulse::CardPort>>,
|
||||||
|
card_profiles: BTreeMap<DeviceId, Vec<pulse::CardProfile>>,
|
||||||
|
active_profiles: BTreeMap<DeviceId, Option<String>>,
|
||||||
|
|
||||||
default_sink: String,
|
default_sink: String,
|
||||||
default_source: String,
|
default_source: String,
|
||||||
|
|
||||||
|
|
@ -77,10 +86,19 @@ pub struct Page {
|
||||||
|
|
||||||
sinks: Vec<String>,
|
sinks: Vec<String>,
|
||||||
sink_ids: Vec<NodeId>,
|
sink_ids: Vec<NodeId>,
|
||||||
|
sink_profiles: Vec<String>,
|
||||||
|
sink_profile_names: Vec<String>,
|
||||||
sources: Vec<String>,
|
sources: Vec<String>,
|
||||||
source_ids: Vec<NodeId>,
|
source_ids: Vec<NodeId>,
|
||||||
|
source_profiles: Vec<String>,
|
||||||
|
source_profile_names: Vec<String>,
|
||||||
|
|
||||||
active_sink: Option<usize>,
|
active_sink: Option<usize>,
|
||||||
|
active_sink_device: Option<DeviceId>,
|
||||||
|
active_sink_profile: Option<usize>,
|
||||||
active_source: Option<usize>,
|
active_source: Option<usize>,
|
||||||
|
active_source_device: Option<DeviceId>,
|
||||||
|
active_source_profile: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl page::Page<crate::pages::Message> for Page {
|
impl page::Page<crate::pages::Message> for Page {
|
||||||
|
|
@ -88,12 +106,7 @@ impl page::Page<crate::pages::Message> for Page {
|
||||||
&self,
|
&self,
|
||||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||||
) -> Option<page::Content> {
|
) -> Option<page::Content> {
|
||||||
Some(vec![
|
Some(vec![sections.insert(output()), sections.insert(input())])
|
||||||
sections.insert(output()),
|
|
||||||
sections.insert(input()),
|
|
||||||
// sections.insert(alerts()),
|
|
||||||
// sections.insert(applications()),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info(&self) -> page::Info {
|
fn info(&self) -> page::Info {
|
||||||
|
|
@ -183,12 +196,53 @@ impl page::Page<crate::pages::Message> for Page {
|
||||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||||
|
|
||||||
impl 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()
|
||||||
|
// TODO: Allow disabling
|
||||||
|
.filter(|p| p.available && p.description != "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) {
|
fn set_default_sink(&mut self) {
|
||||||
for card in self.devices.values() {
|
for (device_id, card) in &self.devices {
|
||||||
if let pipewire::MediaClass::Sink = card.class {
|
for (&node_id, device) in &card.devices {
|
||||||
for (&node_id, profile) in &card.profiles {
|
if let pipewire::MediaClass::Sink = device.class {
|
||||||
if profile.identifier == self.default_sink {
|
if device.identifier == self.default_sink {
|
||||||
self.active_sink = self.sink_ids.iter().position(|&id| id == node_id);
|
self.active_sink = self.sink_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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -197,11 +251,14 @@ impl Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_default_source(&mut self) {
|
fn set_default_source(&mut self) {
|
||||||
for card in self.devices.values() {
|
for (device_id, card) in &self.devices {
|
||||||
if let pipewire::MediaClass::Source = card.class {
|
for (&node_id, device) in &card.devices {
|
||||||
for (&node_id, profile) in &card.profiles {
|
if let pipewire::MediaClass::Source = device.class {
|
||||||
if profile.identifier == self.default_source {
|
if device.identifier == self.default_source {
|
||||||
self.active_source = self.source_ids.iter().position(|&id| id == node_id);
|
self.active_source = self.source_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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -289,13 +346,33 @@ impl Page {
|
||||||
self.source_mute = 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_ports.insert(device_id.clone(), card.ports);
|
||||||
|
self.card_profiles.insert(device_id.clone(), card.profiles);
|
||||||
|
self.active_profiles
|
||||||
|
.insert(device_id, card.active_profile.map(|p| p.name));
|
||||||
|
}
|
||||||
|
|
||||||
Message::Pipewire(pipewire::DeviceEvent::Add(device)) => {
|
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),
|
||||||
|
};
|
||||||
|
|
||||||
match device.media_class {
|
match device.media_class {
|
||||||
pipewire::MediaClass::Sink => {
|
pipewire::MediaClass::Sink => {
|
||||||
self.sinks.push(device.node_description.clone());
|
self.sinks.push(device.node_description.clone());
|
||||||
self.sink_ids.push(device.object_id);
|
self.sink_ids.push(device.object_id);
|
||||||
if self.default_sink == device.node_name {
|
if self.default_sink == device.node_name {
|
||||||
self.active_sink = Some(self.sinks.len() - 1);
|
self.active_sink = Some(self.sinks.len() - 1);
|
||||||
|
self.active_sink_device = Some(device_id.clone());
|
||||||
|
self.set_sink_profiles(&device_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,40 +381,30 @@ impl Page {
|
||||||
self.source_ids.push(device.object_id);
|
self.source_ids.push(device.object_id);
|
||||||
if self.default_source == device.node_name {
|
if self.default_source == device.node_name {
|
||||||
self.active_source = Some(self.sources.len() - 1);
|
self.active_source = Some(self.sources.len() - 1);
|
||||||
|
self.active_source_device = Some(device_id.clone());
|
||||||
|
self.set_source_profiles(&device_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let card = self
|
let card = self.devices.entry(device_id).or_insert_with(|| Card {
|
||||||
.devices
|
devices: BTreeMap::new(),
|
||||||
.entry(match device.variant {
|
});
|
||||||
pipewire::DeviceVariant::Alsa { alsa_card, .. } => {
|
|
||||||
DeviceId::Alsa(alsa_card)
|
|
||||||
}
|
|
||||||
pipewire::DeviceVariant::Bluez5 { address, .. } => {
|
|
||||||
DeviceId::Bluez5(address)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_insert_with(|| Card {
|
|
||||||
class: device.media_class,
|
|
||||||
// name: device.alsa_card_name,
|
|
||||||
profiles: HashMap::new(),
|
|
||||||
});
|
|
||||||
|
|
||||||
card.profiles.insert(
|
card.devices.insert(
|
||||||
device.object_id,
|
device.object_id,
|
||||||
Profile {
|
Port {
|
||||||
// device: device.card_profile_device,
|
class: device.media_class,
|
||||||
identifier: device.node_name,
|
identifier: device.node_name,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::Pipewire(pipewire::DeviceEvent::Remove(device_id)) => {
|
Message::Pipewire(pipewire::DeviceEvent::Remove(node_id)) => {
|
||||||
let mut remove = None;
|
let mut remove = None;
|
||||||
for (card_id, card) in &mut self.devices {
|
for (card_id, card) in &mut self.devices {
|
||||||
if card.profiles.remove(&device_id).is_some() {
|
if card.devices.remove(&node_id).is_some() {
|
||||||
if card.profiles.is_empty() {
|
if card.devices.is_empty() {
|
||||||
remove = Some(card_id.clone());
|
remove = Some(card_id.clone());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -348,17 +415,21 @@ impl Page {
|
||||||
_ = self.devices.remove(&card_id);
|
_ = self.devices.remove(&card_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pos) = self.sink_ids.iter().position(|&id| id == device_id) {
|
if let Some(pos) = self.sink_ids.iter().position(|&id| id == node_id) {
|
||||||
_ = self.sink_ids.remove(pos);
|
_ = self.sink_ids.remove(pos);
|
||||||
_ = self.sinks.remove(pos);
|
_ = self.sinks.remove(pos);
|
||||||
if self.active_sink == Some(pos) {
|
if self.active_sink == Some(pos) {
|
||||||
self.active_sink = None;
|
self.active_sink = None;
|
||||||
|
self.active_sink_device = None;
|
||||||
|
self.active_sink_profile = None;
|
||||||
}
|
}
|
||||||
} else if let Some(pos) = self.source_ids.iter().position(|&id| id == device_id) {
|
} else if let Some(pos) = self.source_ids.iter().position(|&id| id == node_id) {
|
||||||
_ = self.source_ids.remove(pos);
|
_ = self.source_ids.remove(pos);
|
||||||
_ = self.sources.remove(pos);
|
_ = self.sources.remove(pos);
|
||||||
if self.active_source == Some(pos) {
|
if self.active_source == Some(pos) {
|
||||||
self.active_source = None;
|
self.active_source = None;
|
||||||
|
self.active_source_device = None;
|
||||||
|
self.active_source_profile = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -400,6 +471,32 @@ impl Page {
|
||||||
wpctl_set_mute(node_id, self.source_mute);
|
wpctl_set_mute(node_id, self.source_mute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::SinkProfileChanged(profile) => {
|
||||||
|
self.active_sink_profile = Some(profile);
|
||||||
|
if let Some(profile) = self.sink_profile_names.get(profile) {
|
||||||
|
if let Some(ref device_id) = self.active_sink_device {
|
||||||
|
if let Some(name) = self.card_names.get(device_id) {
|
||||||
|
pactl_set_card_profile(name.clone(), profile.clone());
|
||||||
|
self.active_profiles
|
||||||
|
.insert(device_id.clone(), Some(profile.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SourceProfileChanged(profile) => {
|
||||||
|
self.active_source_profile = Some(profile);
|
||||||
|
if let Some(profile) = self.source_profile_names.get(profile) {
|
||||||
|
if let Some(ref device_id) = self.active_source_device {
|
||||||
|
if let Some(name) = self.card_names.get(device_id) {
|
||||||
|
pactl_set_card_profile(name.clone(), profile.clone());
|
||||||
|
self.active_profiles
|
||||||
|
.insert(device_id.clone(), Some(profile.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Command::none()
|
Command::none()
|
||||||
}
|
}
|
||||||
|
|
@ -411,6 +508,7 @@ fn input() -> Section<crate::pages::Message> {
|
||||||
let volume = descriptions.insert(fl!("sound-input", "volume"));
|
let volume = descriptions.insert(fl!("sound-input", "volume"));
|
||||||
let device = descriptions.insert(fl!("sound-input", "device"));
|
let device = descriptions.insert(fl!("sound-input", "device"));
|
||||||
let _level = descriptions.insert(fl!("sound-input", "level"));
|
let _level = descriptions.insert(fl!("sound-input", "level"));
|
||||||
|
let profile = descriptions.insert(fl!("profile"));
|
||||||
|
|
||||||
Section::default()
|
Section::default()
|
||||||
.title(fl!("sound-input"))
|
.title(fl!("sound-input"))
|
||||||
|
|
@ -440,14 +538,24 @@ fn input() -> Section<crate::pages::Message> {
|
||||||
Message::SourceChanged,
|
Message::SourceChanged,
|
||||||
);
|
);
|
||||||
|
|
||||||
settings::view_section(§ion.title)
|
let mut controls = settings::view_section(§ion.title)
|
||||||
.add(settings::item(
|
.add(settings::item(
|
||||||
&*section.descriptions[volume],
|
&*section.descriptions[volume],
|
||||||
volume_control,
|
volume_control,
|
||||||
))
|
))
|
||||||
.add(settings::item(&*section.descriptions[device], devices))
|
.add(settings::item(&*section.descriptions[device], devices));
|
||||||
.apply(Element::from)
|
|
||||||
.map(crate::pages::Message::Sound)
|
if !page.source_profiles.is_empty() {
|
||||||
|
let dropdown = widget::dropdown(
|
||||||
|
&page.source_profiles,
|
||||||
|
page.active_source_profile,
|
||||||
|
Message::SourceProfileChanged,
|
||||||
|
);
|
||||||
|
|
||||||
|
controls = controls.add(settings::item(&*section.descriptions[profile], dropdown));
|
||||||
|
}
|
||||||
|
|
||||||
|
Element::from(controls).map(crate::pages::Message::Sound)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,7 +565,7 @@ fn output() -> Section<crate::pages::Message> {
|
||||||
let volume = descriptions.insert(fl!("sound-output", "volume"));
|
let volume = descriptions.insert(fl!("sound-output", "volume"));
|
||||||
let device = descriptions.insert(fl!("sound-output", "device"));
|
let device = descriptions.insert(fl!("sound-output", "device"));
|
||||||
let _level = descriptions.insert(fl!("sound-output", "level"));
|
let _level = descriptions.insert(fl!("sound-output", "level"));
|
||||||
// let config = descriptions.insert(fl!("sound-output", "config"));
|
let profile = descriptions.insert(fl!("profile"));
|
||||||
// let balance = descriptions.insert(fl!("sound-output", "balance"));
|
// let balance = descriptions.insert(fl!("sound-output", "balance"));
|
||||||
|
|
||||||
Section::default()
|
Section::default()
|
||||||
|
|
@ -488,14 +596,24 @@ fn output() -> Section<crate::pages::Message> {
|
||||||
Message::SinkChanged,
|
Message::SinkChanged,
|
||||||
);
|
);
|
||||||
|
|
||||||
settings::view_section(§ion.title)
|
let mut controls = settings::view_section(§ion.title)
|
||||||
.add(settings::item(
|
.add(settings::item(
|
||||||
&*section.descriptions[volume],
|
&*section.descriptions[volume],
|
||||||
volume_control,
|
volume_control,
|
||||||
))
|
))
|
||||||
.add(settings::item(&*section.descriptions[device], devices))
|
.add(settings::item(&*section.descriptions[device], devices));
|
||||||
.apply(Element::from)
|
|
||||||
.map(crate::pages::Message::Sound)
|
if !page.sink_profiles.is_empty() {
|
||||||
|
let dropdown = widget::dropdown(
|
||||||
|
&page.sink_profiles,
|
||||||
|
page.active_sink_profile,
|
||||||
|
Message::SinkProfileChanged,
|
||||||
|
);
|
||||||
|
|
||||||
|
controls = controls.add(settings::item(&*section.descriptions[profile], dropdown));
|
||||||
|
}
|
||||||
|
|
||||||
|
Element::from(controls).map(crate::pages::Message::Sound)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -533,6 +651,15 @@ fn output() -> Section<crate::pages::Message> {
|
||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
fn pactl_set_card_profile(id: String, profile: String) {
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
_ = tokio::process::Command::new("pactl")
|
||||||
|
.args(&["set-card-profile", id.as_str(), profile.as_str()])
|
||||||
|
.status()
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn wpctl_set_default(id: u32) {
|
fn wpctl_set_default(id: u32) {
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let default = id.to_string();
|
let default = id.to_string();
|
||||||
|
|
@ -543,6 +670,16 @@ fn wpctl_set_default(id: u32) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
fn wpctl_set_volume(id: u32, volume: u32) {
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let id = id.to_string();
|
let id = id.to_string();
|
||||||
|
|
@ -553,13 +690,3 @@ fn wpctl_set_volume(id: u32, volume: u32) {
|
||||||
.await;
|
.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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ impl Default for Page {
|
||||||
let military_time = cosmic_applet_config
|
let military_time = cosmic_applet_config
|
||||||
.get("military_time")
|
.get("military_time")
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
if !matches!(cosmic_config::Error::NoConfigDirectory, err) {
|
if !matches!(err, cosmic_config::Error::NoConfigDirectory) {
|
||||||
error!(?err, "Failed to read config 'military_time'");
|
error!(?err, "Failed to read config 'military_time'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +66,7 @@ impl Default for Page {
|
||||||
let first_day_of_week = cosmic_applet_config
|
let first_day_of_week = cosmic_applet_config
|
||||||
.get("first_day_of_week")
|
.get("first_day_of_week")
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
if !matches!(cosmic_config::Error::NoConfigDirectory, err) {
|
if !matches!(err, cosmic_config::Error::NoConfigDirectory) {
|
||||||
error!(?err, "Failed to read config 'first_day_of_week'");
|
error!(?err, "Failed to read config 'first_day_of_week'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ impl Default for Page {
|
||||||
let show_date_in_top_panel = cosmic_applet_config
|
let show_date_in_top_panel = cosmic_applet_config
|
||||||
.get("show_date_in_top_panel")
|
.get("show_date_in_top_panel")
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
if !matches!(cosmic_config::Error::NoConfigDirectory, err) {
|
if !matches!(err, cosmic_config::Error::NoConfigDirectory) {
|
||||||
error!(?err, "Failed to read config 'show_date_in_top_panel'");
|
error!(?err, "Failed to read config 'show_date_in_top_panel'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,8 @@ sound-alerts = Alerts
|
||||||
sound-applications = Applications
|
sound-applications = Applications
|
||||||
.desc = Application volumes and settings
|
.desc = Application volumes and settings
|
||||||
|
|
||||||
|
profile = Profile
|
||||||
|
|
||||||
## Power
|
## Power
|
||||||
|
|
||||||
power = Power & Battery
|
power = Power & Battery
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue