2025-11-07-23-29
This commit is contained in:
parent
7df8124933
commit
97cb8ff476
16
Behavior.txt
Normal file
16
Behavior.txt
Normal file
@ -0,0 +1,16 @@
|
||||
Full control is taken over everything that is configured
|
||||
and added to Devices.
|
||||
|
||||
On initial creation existing pipewire settings might
|
||||
be copied, but existing configs will not be changed
|
||||
by external interfaces.
|
||||
|
||||
If, for example, a device is configured to use Pro Audio,
|
||||
and you change the device Profile from pavucontrol,
|
||||
it will automatically be changed back.
|
||||
|
||||
This program is supposed to be opinionated with the goal
|
||||
of getting audio devices working as intended. It will
|
||||
not compromise to external software and will force its
|
||||
configurations onto the system. It will provide
|
||||
reproducible endpoints that other programs can attach to.
|
||||
@ -5,13 +5,15 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
dirs = "6.0.0"
|
||||
flume = "0.11.1"
|
||||
flume = { version = "0.11.1", features = ["async"] }
|
||||
gettext-rs = { version = "0.7", features = ["gettext-system"] }
|
||||
gtk = { version = "0.9", package = "gtk4", features = ["gnome_47"] }
|
||||
gtk = { version = "0.9", package = "gtk4", features = ["gnome_47", "v4_18"] }
|
||||
lazy_static = "1.5.0"
|
||||
libspa = "0.8.0"
|
||||
libspa-sys = "0.8.0"
|
||||
pipewire = "0.8.0"
|
||||
sled = "0.34.7"
|
||||
uuid = { version = "1.18.0", features = ["v4", "v7"] }
|
||||
|
||||
[dependencies.adw]
|
||||
package = "libadwaita"
|
||||
|
||||
@ -95,3 +95,96 @@ __________________________
|
||||
| Cancel |
|
||||
| |
|
||||
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
|
||||
|
||||
Device Page
|
||||
|
||||
_______________________________________________________
|
||||
| Devices | Device1 |
|
||||
| | |
|
||||
| Device1 | Pipewire Device Configuration |
|
||||
| | Select audio profiles for specific |
|
||||
| | pipewire devices + |
|
||||
| | _______________________________________ |
|
||||
| | | Device Config 1 | |
|
||||
| | | Device Config 2 | |
|
||||
| | | ... | |
|
||||
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||
| | |
|
||||
| | Inputs |
|
||||
| | Selectable inputs that are mapped |
|
||||
| | to a device + |
|
||||
| | _______________________________________ |
|
||||
| | | Input 1 | |
|
||||
| | | Input 2 | |
|
||||
| | | ... | |
|
||||
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||
| | |
|
||||
| | Outputs |
|
||||
| | Selectable outputs that are mapped |
|
||||
| | to a device + |
|
||||
| | _______________________________________ |
|
||||
| | | Output 1 | |
|
||||
| | | Output 2 | |
|
||||
| | | ... | |
|
||||
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||
| | |
|
||||
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
|
||||
|
||||
Inputs General or Device Bound?
|
||||
|
||||
Device Bound:
|
||||
- It's the input of the device,
|
||||
unlike the device config which is
|
||||
something hardware bound
|
||||
- easier workflow
|
||||
- Could use Simple names instead of
|
||||
sifting through a bunch of inputs
|
||||
from other devices
|
||||
- Since each device might have multiple
|
||||
inputs, the selection screen would
|
||||
get confusing quickly
|
||||
|
||||
Input Page
|
||||
Loopback reference: https://docs.pipewire.org/page_module_loopback.html
|
||||
maybe use commandline tool for config: https://docs.pipewire.org/page_man_pw-loopback_1.html
|
||||
|
||||
possible profiles:
|
||||
- Mono
|
||||
[ MONO ]
|
||||
- Stereo
|
||||
[ FL FR ]
|
||||
- Downmix
|
||||
[ FL FR ]
|
||||
- etc.?
|
||||
- Custom?
|
||||
|
||||
Example (design changes based on selected profile):
|
||||
Stereo, mapped from pro audio
|
||||
|
||||
_______________________________________________________
|
||||
| Devices | < Input 1 |Edit Symbol| |
|
||||
| | __________________________ |
|
||||
| Device1 | Profile | Stereo ⌄ | |
|
||||
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||
| | |
|
||||
| | Input Map |
|
||||
| | Map inputs from a device config |
|
||||
| | to inputs in this profile |
|
||||
| | ________________________ |
|
||||
| | Front Left | DeviceConfig1-Aux0 ⌄ | |
|
||||
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||
| | ________________________ |
|
||||
| | Front Right | DeviceConfig1-Aux1 ⌄ | |
|
||||
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||
| | |
|
||||
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
|
||||
|
||||
Output Page
|
||||
|
||||
possible profiles:
|
||||
- Mono
|
||||
[
|
||||
- Stereo
|
||||
- Upmix (with param configuration)
|
||||
- etc.?
|
||||
- Custom?
|
||||
@ -18,14 +18,16 @@
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
extern crate core;
|
||||
|
||||
mod application;
|
||||
mod config;
|
||||
mod window;
|
||||
mod components;
|
||||
mod pw_manager;
|
||||
mod pipewire_manager;
|
||||
mod utils;
|
||||
mod state_manager;
|
||||
mod spa_structs;
|
||||
|
||||
use self::application::AudioDeviceManagerApplication;
|
||||
use self::window::AudioDeviceManagerWindow;
|
||||
|
||||
676
src/pipewire_manager.rs
Normal file
676
src/pipewire_manager.rs
Normal file
@ -0,0 +1,676 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::rc::Rc;
|
||||
use std::thread;
|
||||
use std::thread::{sleep, JoinHandle};
|
||||
use std::time::Duration;
|
||||
use adw::glib;
|
||||
use adw::glib::{clone, GString, ParamFlags, Value};
|
||||
use adw::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
use gtk::glib::ExitCode;
|
||||
use libspa::pod::Pod;
|
||||
use pipewire::{context::Context, main_loop::MainLoop};
|
||||
use pipewire::properties::{properties, Properties};
|
||||
use pipewire::registry::{GlobalObject, Registry};
|
||||
use pipewire::spa::param::ParamType;
|
||||
|
||||
use crate::pipewire_manager::deserialize_pod::deserialize_pod;
|
||||
|
||||
type DeviceId = usize;
|
||||
|
||||
enum DeviceMessage {
|
||||
ProfileDiscovered(DeviceProfile),
|
||||
ActiveProfileSet(ProfileId),
|
||||
PropertyDiscovered(String, String),
|
||||
}
|
||||
|
||||
enum Data {
|
||||
DeviceMessage(DeviceId, DeviceMessage)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DeviceCommand {
|
||||
SetProfile(ProfileId)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Command {
|
||||
DeviceCommand(DeviceId, DeviceCommand)
|
||||
}
|
||||
|
||||
struct RegisteredObjects {
|
||||
profile_combo_rows: HashMap<uuid::Uuid, (usize, adw::ComboRow, gtk::StringList)>
|
||||
}
|
||||
|
||||
impl RegisteredObjects {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
profile_combo_rows: HashMap::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Device {
|
||||
properties: HashMap<String, String>,
|
||||
profiles: HashMap<usize, DeviceProfile>,
|
||||
active_profile: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for Device {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
properties: HashMap::new(),
|
||||
profiles: HashMap::new(),
|
||||
active_profile: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PipewireManager {
|
||||
handle: JoinHandle<ExitCode>,
|
||||
devices: Rc<RefCell<HashMap<usize, Device>>>,
|
||||
registered_objects: Rc<RefCell<RegisteredObjects>>,
|
||||
tx: pipewire::channel::Sender<Command>,
|
||||
rx: flume::Receiver<Data>
|
||||
}
|
||||
|
||||
impl Default for PipewireManager {
|
||||
fn default() -> Self {
|
||||
let (handle, tx, rx) = spawn_pipewire_thread();
|
||||
|
||||
let devices = Rc::new(RefCell::new(HashMap::new()));
|
||||
let registered_objects = Rc::new(RefCell::new(RegisteredObjects::new()));
|
||||
|
||||
let rx_clone = rx.clone();
|
||||
|
||||
glib::spawn_future_local(clone!(
|
||||
#[weak]
|
||||
devices,
|
||||
#[weak]
|
||||
registered_objects,
|
||||
async move {
|
||||
handle_registered_objects(rx_clone, devices, registered_objects).await
|
||||
}
|
||||
));
|
||||
|
||||
let instance = Self {
|
||||
handle,
|
||||
devices,
|
||||
registered_objects,
|
||||
tx,
|
||||
rx
|
||||
};
|
||||
|
||||
instance
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_registered_objects(
|
||||
rx: flume::Receiver<Data>,
|
||||
devices: Rc<RefCell<HashMap<usize, Device>>>,
|
||||
registered_objects: Rc<RefCell<RegisteredObjects>>
|
||||
) {
|
||||
let mut active_profile_set = None;
|
||||
while let Ok(data) = rx.recv_async().await {
|
||||
match data {
|
||||
Data::DeviceMessage(device_id, message) => {
|
||||
let mut devices_mut = devices.borrow_mut();
|
||||
let device = devices_mut
|
||||
.entry(device_id)
|
||||
.or_insert(Device::default());
|
||||
|
||||
match message {
|
||||
DeviceMessage::ProfileDiscovered(profile) => {
|
||||
let profile_name = profile.description.clone();
|
||||
device.profiles.insert(profile.id, profile);
|
||||
handle_profile_discovered(device_id, registered_objects.clone(), profile_name);
|
||||
}
|
||||
DeviceMessage::ActiveProfileSet(id) => {
|
||||
println!("Active profile set: {}", id);
|
||||
active_profile_set = Some(id);
|
||||
}
|
||||
DeviceMessage::PropertyDiscovered(k, v) => {
|
||||
device.properties.insert(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = active_profile_set {
|
||||
if let Some(profile) = device.profiles.get(&id) {
|
||||
device.active_profile = Some(id);
|
||||
let profile_name = profile.description.clone();
|
||||
handle_active_profile_set(device_id, registered_objects.clone(), profile_name);
|
||||
active_profile_set = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_profile_discovered(device_id: usize, registered_objects: Rc<RefCell<RegisteredObjects>>, profile_name: String) {
|
||||
for (
|
||||
_id,
|
||||
(
|
||||
combo_row_device_id,
|
||||
_combo_row,
|
||||
model
|
||||
)
|
||||
) in &mut registered_objects.borrow_mut().profile_combo_rows {
|
||||
|
||||
if *combo_row_device_id != device_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
while let Some(p) = model.string(i) {
|
||||
match p.to_string().cmp(&profile_name) {
|
||||
Ordering::Less => {}
|
||||
Ordering::Equal => return,
|
||||
Ordering::Greater => {break;}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
model.splice(i, 0, &[&profile_name]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_active_profile_set(device_id: usize, registered_objects: Rc<RefCell<RegisteredObjects>>, profile_name: String) {
|
||||
for (
|
||||
_id,
|
||||
(
|
||||
combo_row_device_id,
|
||||
combo_row,
|
||||
model
|
||||
)
|
||||
) in &mut registered_objects.borrow_mut().profile_combo_rows {
|
||||
if *combo_row_device_id != device_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let i = model.find(&profile_name);
|
||||
|
||||
combo_row.set_selected(i);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl PipewireManager {
|
||||
pub(crate) fn get_device_names(&self) -> HashMap<usize, String> {
|
||||
self.devices.borrow()
|
||||
.iter()
|
||||
.map(|(id, d)| {
|
||||
(
|
||||
*id,
|
||||
d.properties
|
||||
.get("device.description")
|
||||
.unwrap_or(&"Error loading device name".to_owned())
|
||||
.to_owned()
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get_device_properties(&self, id: usize) -> HashMap<String, String> {
|
||||
self.devices.borrow()[&id]
|
||||
.properties
|
||||
.clone()
|
||||
|
||||
}
|
||||
|
||||
pub(crate) fn get_device_ids_from_properties(&self, properties: HashMap<String, String>) -> Vec<usize> {
|
||||
self.devices.borrow()
|
||||
.iter()
|
||||
.filter(
|
||||
|(id, d)| {
|
||||
properties
|
||||
.iter()
|
||||
.all(|(k, v)| {
|
||||
let Some(v2) = d.properties.get(k) else { return false; };
|
||||
v == v2
|
||||
})
|
||||
}
|
||||
)
|
||||
.map(|(id, d)| *id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn register_profile_combo_row(&self, device_id: usize, combo_row: adw::ComboRow) -> uuid::Uuid {
|
||||
let mut profile_names: Vec<_> = self.devices.borrow()[&device_id].profiles
|
||||
.iter()
|
||||
.map(|(id, p)| {
|
||||
p.description.clone()
|
||||
})
|
||||
.collect();
|
||||
|
||||
profile_names.sort();
|
||||
|
||||
let active_profile = self.devices.borrow()[&device_id].active_profile
|
||||
.clone() // make sure no lockups happen
|
||||
.and_then(|id| {
|
||||
Some((id, self.devices.borrow()[&device_id].profiles[&id].description.clone()))
|
||||
});
|
||||
|
||||
let model = gtk::StringList::new(&[]);
|
||||
for name in &profile_names {
|
||||
model.append(name);
|
||||
}
|
||||
|
||||
combo_row.set_model(Some(&model));
|
||||
|
||||
combo_row.connect_selected_notify(clone!(
|
||||
#[weak]
|
||||
model,
|
||||
#[weak(rename_to=devices)]
|
||||
self.devices,
|
||||
#[strong(rename_to=tx)]
|
||||
self.tx,
|
||||
move |c| {
|
||||
let model_entry = c.selected();
|
||||
let selected_profile_name = model.string(model_entry).unwrap().to_string();
|
||||
|
||||
let mut selected_profile_id = None;
|
||||
for (profile_id, profile) in &devices.borrow()[&device_id].profiles {
|
||||
if selected_profile_name == profile.description {
|
||||
selected_profile_id = Some(*profile_id);
|
||||
};
|
||||
};
|
||||
|
||||
let selected_profile_id = selected_profile_id.unwrap();
|
||||
|
||||
tx.send(
|
||||
Command::DeviceCommand(device_id, DeviceCommand::SetProfile(selected_profile_id))
|
||||
).unwrap();
|
||||
}
|
||||
));
|
||||
|
||||
if let Some((_, name)) = active_profile {
|
||||
if let Ok(position) = profile_names.binary_search(&&name) {
|
||||
combo_row.set_selected(position as u32)
|
||||
}
|
||||
}
|
||||
|
||||
let combo_row_id = uuid::Uuid::new_v4();
|
||||
|
||||
self.registered_objects.borrow_mut().profile_combo_rows.insert(combo_row_id, (device_id, combo_row, model));
|
||||
|
||||
combo_row_id
|
||||
}
|
||||
}
|
||||
|
||||
enum DeviceAvailability {
|
||||
Available,
|
||||
Unavailable,
|
||||
Unknown
|
||||
}
|
||||
|
||||
struct DeviceProfile {
|
||||
id: usize,
|
||||
name: String,
|
||||
description: String,
|
||||
priority: usize,
|
||||
available: DeviceAvailability,
|
||||
}
|
||||
|
||||
type ProfileId = usize;
|
||||
type PodBytes = Vec<u8>;
|
||||
|
||||
// Devices don't have to be connected to current system
|
||||
// Relevant device data has to be stored
|
||||
// User is supposed to be able to pick out which device data is relevant and identifying
|
||||
// default is device.bus-path or device.product.id
|
||||
// could also be device.product.id + device.product.name + device.vendor.name
|
||||
// Interface: List of all device properties and next to them a checkbox for "relevant identifier"
|
||||
// Maybe also allow regex
|
||||
// also maybe allow custom script
|
||||
struct DeviceMessageHandler {
|
||||
pipewire_device: Rc<RefCell<pipewire::device::Device>>,
|
||||
device_listener: pipewire::device::DeviceListener,
|
||||
profiles: Rc<RefCell<HashMap<ProfileId, PodBytes>>>,
|
||||
}
|
||||
|
||||
impl DeviceMessageHandler {
|
||||
fn new(pipewire_device: pipewire::device::Device, id: usize, tx: flume::Sender<Data>) -> Self {
|
||||
|
||||
let pipewire_device_rc = Rc::new(RefCell::new(pipewire_device));
|
||||
let profiles = Rc::new(RefCell::new(HashMap::new()));
|
||||
|
||||
let pipewire_device_ref = pipewire_device_rc.borrow();
|
||||
let mut listener_builder = pipewire_device_ref.add_listener_local();
|
||||
|
||||
listener_builder = Self::listen_for_params(id, listener_builder, tx.clone(), profiles.clone());
|
||||
listener_builder = Self::listen_for_info(id, listener_builder, tx, pipewire_device_rc.clone());
|
||||
|
||||
|
||||
let device_listener = listener_builder.register();
|
||||
|
||||
pipewire_device_ref.subscribe_params(&[
|
||||
ParamType::Profile,
|
||||
ParamType::EnumProfile,
|
||||
]);
|
||||
|
||||
drop(pipewire_device_ref);
|
||||
|
||||
Self {
|
||||
pipewire_device: pipewire_device_rc,
|
||||
device_listener,
|
||||
profiles
|
||||
}
|
||||
}
|
||||
|
||||
fn listen_for_params(
|
||||
device_id: usize,
|
||||
listener_builder: pipewire::device::DeviceListenerLocalBuilder,
|
||||
tx: flume::Sender<Data>,
|
||||
profiles: Rc<RefCell<HashMap<ProfileId, PodBytes>>>
|
||||
) -> pipewire::device::DeviceListenerLocalBuilder {
|
||||
listener_builder
|
||||
.param(clone!(
|
||||
#[weak]
|
||||
profiles,
|
||||
move |seq, param_type, index, next, param_pod| {
|
||||
|
||||
let Some(pod) = param_pod else { return };
|
||||
|
||||
let Some(param) = deserialize_pod(param_pod) else { return };
|
||||
|
||||
Self::on_profile_discovered(
|
||||
device_id,
|
||||
param,
|
||||
param_type,
|
||||
tx.clone(),
|
||||
profiles,
|
||||
pod
|
||||
)
|
||||
}
|
||||
|
||||
))
|
||||
}
|
||||
|
||||
fn listen_for_info(
|
||||
device_id: usize,
|
||||
listener_builder: pipewire::device::DeviceListenerLocalBuilder,
|
||||
tx: flume::Sender<Data>,
|
||||
pipewire_device: Rc<RefCell<pipewire::device::Device>>
|
||||
) -> pipewire::device::DeviceListenerLocalBuilder {
|
||||
listener_builder
|
||||
.info(clone!(
|
||||
#[weak]
|
||||
pipewire_device,
|
||||
move |info| {
|
||||
Self::on_info_discovered(
|
||||
device_id,
|
||||
info,
|
||||
tx.clone(),
|
||||
pipewire_device
|
||||
)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fn on_profile_discovered(
|
||||
device_id: usize,
|
||||
param: libspa::pod::Object,
|
||||
param_type: ParamType,
|
||||
tx: flume::Sender<Data>,
|
||||
profiles: Rc<RefCell<HashMap<ProfileId, PodBytes>>>,
|
||||
pod: &Pod
|
||||
) {
|
||||
let mut profile_id = None;
|
||||
let mut profile_name = None;
|
||||
let mut profile_description = None;
|
||||
let mut profile_priority = None;
|
||||
let mut profile_availability = None;
|
||||
|
||||
for property in param.properties {
|
||||
match (property.key, property.value) {
|
||||
(1, libspa::pod::Value::Int(id)) => { profile_id = Some(id as usize); },
|
||||
(2, libspa::pod::Value::String(name)) => { profile_name = Some(name); },
|
||||
(3, libspa::pod::Value::String(description)) => { profile_description = Some(description); },
|
||||
(4, libspa::pod::Value::Int(priority)) => { profile_priority = Some(priority as usize); },
|
||||
(5, libspa::pod::Value::Id(libspa::utils::Id(0))) => { profile_availability = Some(DeviceAvailability::Unknown); },
|
||||
(5, libspa::pod::Value::Id(libspa::utils::Id(1))) => { profile_availability = Some(DeviceAvailability::Unavailable); },
|
||||
(5, libspa::pod::Value::Id(libspa::utils::Id(2))) => { profile_availability = Some(DeviceAvailability::Available); },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Proper error handling
|
||||
|
||||
match param_type {
|
||||
ParamType::Profile => {
|
||||
let id = profile_id.unwrap();
|
||||
tx.send(
|
||||
Data::DeviceMessage(
|
||||
device_id,
|
||||
DeviceMessage::ActiveProfileSet(id)
|
||||
)
|
||||
)
|
||||
.unwrap();
|
||||
},
|
||||
ParamType::EnumProfile => {
|
||||
let id = profile_id.unwrap();
|
||||
|
||||
let pod_bytes = pod.as_bytes().to_vec();
|
||||
profiles.borrow_mut().insert(id, pod_bytes);
|
||||
|
||||
let device_profile = DeviceProfile {
|
||||
id,
|
||||
name: profile_name.unwrap(),
|
||||
description: profile_description.unwrap(),
|
||||
priority: profile_priority.unwrap(),
|
||||
available: profile_availability.unwrap(),
|
||||
};
|
||||
tx.send(
|
||||
Data::DeviceMessage(
|
||||
device_id,
|
||||
DeviceMessage::ProfileDiscovered(device_profile)
|
||||
)
|
||||
)
|
||||
.unwrap();
|
||||
},
|
||||
_ => return
|
||||
}
|
||||
}
|
||||
|
||||
fn on_info_discovered(
|
||||
device_id: usize,
|
||||
info: &pipewire::device::DeviceInfoRef,
|
||||
tx: flume::Sender<Data>,
|
||||
pipewire_device: Rc<RefCell<pipewire::device::Device>>
|
||||
) {
|
||||
// a bit jank, but works for now
|
||||
// every time an info update is given also give a params update
|
||||
// because subscribed params don't work somehow
|
||||
pipewire_device.borrow().enum_params(1, Some(ParamType::Profile), 0, 1);
|
||||
info.props().unwrap().iter()
|
||||
.for_each(|(k, v)| {
|
||||
tx.send(Data::DeviceMessage(
|
||||
device_id,
|
||||
DeviceMessage::PropertyDiscovered(k.to_owned(), v.to_owned())
|
||||
)).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
fn set_profile(&self, profile_id: ProfileId) {
|
||||
let profile_pod_bytes = match self.profiles.borrow().get(&profile_id) {
|
||||
Some(pod_bytes) => pod_bytes.clone(),
|
||||
None => {
|
||||
println!("No profile with id {} found", profile_id);
|
||||
return
|
||||
}
|
||||
};
|
||||
|
||||
let profile_pod = Pod::from_bytes(&profile_pod_bytes).unwrap();
|
||||
|
||||
self.pipewire_device.borrow().set_param(ParamType::Profile, ParamFlags::empty().bits(), profile_pod)
|
||||
}
|
||||
}
|
||||
|
||||
struct GlobalsManager {
|
||||
globals: Vec<GlobalObject<Properties>>,
|
||||
devices: HashMap<usize, DeviceMessageHandler>,
|
||||
tx: flume::Sender<Data>,
|
||||
}
|
||||
|
||||
impl GlobalsManager {
|
||||
fn new(tx: flume::Sender<Data>) -> Self {
|
||||
Self {
|
||||
globals: Vec::new(),
|
||||
devices: HashMap::new(),
|
||||
tx
|
||||
}
|
||||
}
|
||||
|
||||
fn add_global(&mut self, global: GlobalObject<Properties>, registry: &Registry) {
|
||||
self.globals.push(global);
|
||||
|
||||
let global_ref = self.globals.last().unwrap();
|
||||
|
||||
let props = match &global_ref.props {
|
||||
Some(props) => props,
|
||||
None => return
|
||||
};
|
||||
|
||||
match props.get("media.class") {
|
||||
Some("Audio/Device") => {}
|
||||
_ => return
|
||||
}
|
||||
|
||||
let device_id = global_ref.id as usize;
|
||||
|
||||
let device = DeviceMessageHandler::new(registry.bind(global_ref).unwrap(), device_id, self.tx.clone());
|
||||
|
||||
self.devices.insert(device_id, device);
|
||||
|
||||
println!("ADDED DEVICE");
|
||||
}
|
||||
|
||||
fn get_device(&self, device_id: usize) -> Option<&DeviceMessageHandler> {
|
||||
self.devices.get(&device_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_pipewire_thread() -> (JoinHandle<ExitCode>, pipewire::channel::Sender<Command>, flume::Receiver<Data>) {
|
||||
|
||||
let (tx, remote_rx) = flume::unbounded();
|
||||
let (remote_tx, rx) = pipewire::channel::channel();
|
||||
|
||||
let pw_thread = thread::spawn(move || {
|
||||
// Initialize PipeWire and run the main loop
|
||||
// ...
|
||||
let mainloop = MainLoop::new(None).expect("failed to get mail loop");
|
||||
let context = Context::new(&mainloop).expect("failed to get context");
|
||||
let core = context.connect(None).expect("failed to get core");
|
||||
let registry = Rc::new(core.get_registry().expect("failed to get registry"));
|
||||
|
||||
let globals_manager = Rc::new(RefCell::new(GlobalsManager::new(tx.clone())));
|
||||
|
||||
|
||||
let _listener = registry
|
||||
.add_listener_local()
|
||||
.global(clone!(
|
||||
#[strong]
|
||||
registry,
|
||||
#[strong]
|
||||
globals_manager,
|
||||
move |global|
|
||||
{
|
||||
let owned_global = global.to_owned();
|
||||
|
||||
|
||||
globals_manager.borrow_mut().add_global(owned_global, ®istry);
|
||||
|
||||
/*
|
||||
if global.type_ == ObjectType::Port {
|
||||
let props = global.props.as_ref().unwrap();
|
||||
let port_name = props.get("port.name");
|
||||
let port_alias = props.get("port.alias");
|
||||
let object_path = props.get("object.path");
|
||||
let format_dsp = props.get("format.dsp");
|
||||
let audio_channel = props.get("audio.channel");
|
||||
let port_id = props.get("port.id");
|
||||
let port_direction = props.get("port.direction");
|
||||
println!("Port: Name: {:?} Alias: {:?} Id: {:?} Direction: {:?} AudioChannel: {:?} Object Path: {:?} FormatDsp: {:?}",
|
||||
port_name,
|
||||
port_alias,
|
||||
port_id,port_direction,audio_channel,object_path,format_dsp
|
||||
);
|
||||
} else if global.type_ == ObjectType::Device {
|
||||
let props = global.props.as_ref().unwrap();
|
||||
let device_name = props.get("device.name");
|
||||
let device_nick = props.get("device.nick");
|
||||
let device_description = props.get("device.description");
|
||||
let device_api = props.get("device.api");
|
||||
let media_class = props.get("media.class");
|
||||
println!("Device: Name: {:?} Nick: {:?} Desc: {:?} Api: {:?} MediaClass: {:?}",
|
||||
device_name, device_nick, device_description, device_api, media_class);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
)
|
||||
)
|
||||
.register();
|
||||
|
||||
|
||||
|
||||
let _receiver = rx.attach(mainloop.loop_(), {
|
||||
let mainloop = mainloop.clone();
|
||||
{
|
||||
let globals_manager_weak = Rc::downgrade(&globals_manager);
|
||||
move |command| {
|
||||
let Some(globals_manager) = globals_manager_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match command {
|
||||
Command::DeviceCommand(device_id, device_command) => {
|
||||
let globals_manager_ref = globals_manager.borrow();
|
||||
let Some(device) = globals_manager_ref.get_device(device_id) else {
|
||||
println!("No device with id {} found", device_id);
|
||||
return
|
||||
};
|
||||
match device_command {
|
||||
DeviceCommand::SetProfile(profile_id) => {
|
||||
device.set_profile(profile_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calling the `destroy_global` method on the registry will destroy the object with the specified id on the remote.
|
||||
// We don't have a specific object to destroy now, so this is commented out.
|
||||
// registry.destroy_global(313).into_result()?;
|
||||
|
||||
|
||||
mainloop.run();
|
||||
return ExitCode::FAILURE; // not sure
|
||||
});
|
||||
|
||||
(pw_thread, remote_tx, remote_rx)
|
||||
}
|
||||
|
||||
mod deserialize_pod {
|
||||
/// Taken from wiremix: https://github.com/tsowell/wiremix/blob/main/src/wirehose/deserialize.rs#L6
|
||||
use libspa::pod::{deserialize::PodDeserializer, Object, Pod, Value};
|
||||
pub fn deserialize_pod(param: Option<&Pod>) -> Option<Object> {
|
||||
param
|
||||
.and_then(|pod| {
|
||||
PodDeserializer::deserialize_any_from(pod.as_bytes()).ok()
|
||||
})
|
||||
.and_then(|(_, value)| match value {
|
||||
Value::Object(obj) => Some(obj),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,410 +0,0 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::rc::Rc;
|
||||
use std::thread;
|
||||
use std::thread::{sleep, JoinHandle};
|
||||
use std::time::Duration;
|
||||
use gtk::glib::ExitCode;
|
||||
use pipewire::{context::Context, main_loop::MainLoop};
|
||||
use pipewire::properties::Properties;
|
||||
use pipewire::registry::{GlobalObject, Registry};
|
||||
use pipewire::spa::param::ParamType;
|
||||
|
||||
use crate::pw_manager::deserialize::deserialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Command {
|
||||
GetDeviceNames,
|
||||
GetDeviceProperties(usize),
|
||||
}
|
||||
|
||||
enum Data {
|
||||
DeviceNames(HashMap<usize, String>),
|
||||
DeviceProperties(usize, HashMap<String, String>),
|
||||
}
|
||||
|
||||
pub(crate) struct PipewireManager {
|
||||
handle: JoinHandle<ExitCode>,
|
||||
tx: pipewire::channel::Sender<Command>,
|
||||
rx: flume::Receiver<Data>,
|
||||
}
|
||||
|
||||
impl Default for PipewireManager {
|
||||
fn default() -> Self {
|
||||
let (handle, tx, rx) = spawn_pipewire_thread();
|
||||
|
||||
Self {
|
||||
handle,
|
||||
tx,
|
||||
rx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PipewireManager {
|
||||
pub(crate) fn get_device_names(&self) -> HashMap<usize, String> {
|
||||
self.tx.send(Command::GetDeviceNames).unwrap();
|
||||
match self.rx.recv().unwrap() {
|
||||
Data::DeviceNames(names) => names,
|
||||
_ => panic!("Invalid data")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_device_properties(&self, id: usize) -> HashMap<String, String> {
|
||||
self.tx.send(Command::GetDeviceProperties(id)).unwrap();
|
||||
match self.rx.recv().unwrap() {
|
||||
Data::DeviceProperties(_, properties) => properties,
|
||||
_ => panic!("Invalid data")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DeviceAvailability {
|
||||
Available,
|
||||
Unavailable,
|
||||
Unknown
|
||||
}
|
||||
|
||||
struct DeviceProfile {
|
||||
id: usize,
|
||||
name: String,
|
||||
description: String,
|
||||
priority: usize,
|
||||
available: DeviceAvailability,
|
||||
}
|
||||
|
||||
// Devices don't have to be connected to current system
|
||||
// Relevant device data has to be stored
|
||||
// User is supposed to be able to pick out which device data is relevant and identifying
|
||||
// default is device.bus-path or device.product.id
|
||||
// could also be device.product.id + device.product.name + device.vendor.name
|
||||
// Interface: List of all device properties and next to them a checkbox for "relevant identifier"
|
||||
// Maybe also allow regex
|
||||
// also maybe allow custom script
|
||||
struct Device {
|
||||
active_profile: Rc<RefCell<Option<usize>>>,
|
||||
profiles: Rc<RefCell<HashMap<usize, DeviceProfile>>>,
|
||||
name: Rc<RefCell<String>>,
|
||||
id: usize,
|
||||
properties: Rc<RefCell<HashMap<String, String>>>,
|
||||
pipewire_device: pipewire::device::Device,
|
||||
device_listener: pipewire::device::DeviceListener,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
fn new(pipewire_device: pipewire::device::Device, id: usize) -> Self {
|
||||
|
||||
let active_profile = Rc::new(RefCell::new(None));
|
||||
let profiles = Rc::new(RefCell::new(HashMap::new()));
|
||||
let name = Rc::new(RefCell::new(String::new()));
|
||||
let properties = Rc::new(RefCell::new(HashMap::new()));
|
||||
|
||||
let mut listener_builder = pipewire_device.add_listener_local();
|
||||
|
||||
listener_builder= Self::listen_for_params(listener_builder, &active_profile, &profiles);
|
||||
listener_builder= Self::listen_for_info(listener_builder, &name, &properties);
|
||||
|
||||
|
||||
let device_listener = listener_builder.register();
|
||||
|
||||
|
||||
pipewire_device.subscribe_params(&[
|
||||
ParamType::Profile,
|
||||
ParamType::EnumProfile
|
||||
]);
|
||||
|
||||
Self {
|
||||
active_profile,
|
||||
profiles,
|
||||
name,
|
||||
id,
|
||||
properties,
|
||||
pipewire_device,
|
||||
device_listener
|
||||
}
|
||||
}
|
||||
|
||||
fn listen_for_params<'a>(
|
||||
listener_builder: pipewire::device::DeviceListenerLocalBuilder<'a>,
|
||||
active_profile: &Rc<RefCell<Option<usize>>>,
|
||||
profiles: &Rc<RefCell<HashMap<usize, DeviceProfile>>>,
|
||||
) -> pipewire::device::DeviceListenerLocalBuilder<'a> {
|
||||
listener_builder
|
||||
.param({
|
||||
let active_profile_weak = Rc::downgrade(active_profile);
|
||||
let profiles_weak = Rc::downgrade(profiles);
|
||||
|
||||
move |seq, param_type, index, next, param| {
|
||||
|
||||
let Some(active_profile) = active_profile_weak.upgrade() else { return };
|
||||
let Some(profiles) = profiles_weak.upgrade() else { return };
|
||||
|
||||
let Some(param) = deserialize(param) else { return };
|
||||
|
||||
Self::on_profile_discovered(
|
||||
param,
|
||||
param_type,
|
||||
active_profile,
|
||||
profiles
|
||||
)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fn listen_for_info<'a>(
|
||||
listener_builder: pipewire::device::DeviceListenerLocalBuilder<'a>,
|
||||
name: &Rc<RefCell<String>>,
|
||||
properties: &Rc<RefCell<HashMap<String, String>>>,
|
||||
) -> pipewire::device::DeviceListenerLocalBuilder<'a> {
|
||||
listener_builder
|
||||
.info({
|
||||
|
||||
let name_weak = Rc::downgrade(name);
|
||||
let properties_weak = Rc::downgrade(properties);
|
||||
|
||||
move |info| {
|
||||
|
||||
let Some(name) = name_weak.upgrade() else { return };
|
||||
let Some(properties) = properties_weak.upgrade() else { return };
|
||||
|
||||
Self::on_info_discovered(
|
||||
info,
|
||||
name,
|
||||
properties
|
||||
)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fn on_profile_discovered(
|
||||
param: libspa::pod::Object,
|
||||
param_type: ParamType,
|
||||
active_profile: Rc<RefCell<Option<usize>>>,
|
||||
profiles: Rc<RefCell<HashMap<usize, DeviceProfile>>>,
|
||||
) {
|
||||
let mut profile_id = None;
|
||||
let mut profile_name = None;
|
||||
let mut profile_description = None;
|
||||
let mut profile_priority = None;
|
||||
let mut profile_availibility = None;
|
||||
|
||||
for property in param.properties {
|
||||
match (property.key, property.value) {
|
||||
(1, libspa::pod::Value::Int(id)) => { profile_id = Some(id as usize); },
|
||||
(2, libspa::pod::Value::String(name)) => { profile_name = Some(name); },
|
||||
(3, libspa::pod::Value::String(description)) => { profile_description = Some(description); },
|
||||
(4, libspa::pod::Value::Int(priority)) => { profile_priority = Some(priority as usize); },
|
||||
(5, libspa::pod::Value::Id(libspa::utils::Id(0))) => { profile_availibility = Some(DeviceAvailability::Unknown); },
|
||||
(5, libspa::pod::Value::Id(libspa::utils::Id(1))) => { profile_availibility = Some(DeviceAvailability::Unavailable); },
|
||||
(5, libspa::pod::Value::Id(libspa::utils::Id(2))) => { profile_availibility = Some(DeviceAvailability::Available); },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Proper error handling
|
||||
|
||||
match param_type {
|
||||
ParamType::Profile => {
|
||||
active_profile.borrow_mut().replace(profile_id.unwrap());
|
||||
},
|
||||
ParamType::EnumProfile => {
|
||||
let device_profile = DeviceProfile {
|
||||
id: profile_id.unwrap(),
|
||||
name: profile_name.unwrap(),
|
||||
description: profile_description.unwrap(),
|
||||
priority: profile_priority.unwrap(),
|
||||
available: profile_availibility.unwrap(),
|
||||
};
|
||||
profiles.borrow_mut().insert(profile_id.unwrap(), device_profile);
|
||||
},
|
||||
_ => return
|
||||
}
|
||||
}
|
||||
|
||||
fn on_info_discovered(
|
||||
info: &pipewire::device::DeviceInfoRef,
|
||||
name: Rc<RefCell<String>>,
|
||||
properties: Rc<RefCell<HashMap<String, String>>>,
|
||||
) {
|
||||
info.props().unwrap().iter()
|
||||
.for_each(|(k, v)| {
|
||||
if k == "device.description" {
|
||||
*name.borrow_mut() = v.to_string();
|
||||
}
|
||||
properties.borrow_mut().insert(k.to_string(), v.to_string());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct GlobalsManager {
|
||||
globals: Vec<GlobalObject<Properties>>,
|
||||
devices: HashMap<usize, Device>,
|
||||
}
|
||||
|
||||
impl GlobalsManager {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
globals: Vec::new(),
|
||||
devices: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_global(&mut self, global: GlobalObject<Properties>, registry: &Registry) {
|
||||
self.globals.push(global);
|
||||
|
||||
let global_ref = self.globals.last().unwrap();
|
||||
|
||||
let props = match &global_ref.props {
|
||||
Some(props) => props,
|
||||
None => return
|
||||
};
|
||||
|
||||
match props.get("media.class") {
|
||||
Some("Audio/Device") => {}
|
||||
_ => return
|
||||
}
|
||||
|
||||
let device_id = global_ref.id as usize;
|
||||
|
||||
let device = Device::new(registry.bind(global_ref).unwrap(), device_id);
|
||||
|
||||
self.devices.insert(device_id, device);
|
||||
|
||||
println!("ADDED DEVICE");
|
||||
}
|
||||
|
||||
fn get_device_names(&self) -> HashMap<usize, String> {
|
||||
self.devices.iter()
|
||||
.map(|(id, d)| (*id, d.name.borrow().clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_device_properties(&self, id: usize) -> HashMap<String, String> {
|
||||
self.devices.get(&id)
|
||||
.and_then(|d| Some(d.properties.borrow().clone()))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_pipewire_thread() -> (JoinHandle<ExitCode>, pipewire::channel::Sender<Command>, flume::Receiver<Data>) {
|
||||
|
||||
let (tx, remote_rx) = flume::unbounded();
|
||||
let (remote_tx, rx) = pipewire::channel::channel();
|
||||
|
||||
let pw_thread = thread::spawn(|| {
|
||||
// Initialize PipeWire and run the main loop
|
||||
// ...
|
||||
let mainloop = MainLoop::new(None).expect("failed to get mail loop");
|
||||
let context = Context::new(&mainloop).expect("failed to get context");
|
||||
let core = context.connect(None).expect("failed to get core");
|
||||
let registry = Rc::new(core.get_registry().expect("failed to get registry"));
|
||||
|
||||
let globals_manager = Rc::new(RefCell::new(GlobalsManager::new()));
|
||||
|
||||
|
||||
let _listener = registry
|
||||
.add_listener_local()
|
||||
.global({
|
||||
let registry_weak = Rc::downgrade(®istry);
|
||||
let globals_manager_weak = Rc::downgrade(&globals_manager);
|
||||
move |global|
|
||||
{
|
||||
|
||||
let Some(registry) = registry_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(globals_manager) = globals_manager_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let owned_global = global.to_owned();
|
||||
|
||||
|
||||
globals_manager.borrow_mut().add_global(owned_global, ®istry);
|
||||
|
||||
/*
|
||||
if global.type_ == ObjectType::Port {
|
||||
let props = global.props.as_ref().unwrap();
|
||||
let port_name = props.get("port.name");
|
||||
let port_alias = props.get("port.alias");
|
||||
let object_path = props.get("object.path");
|
||||
let format_dsp = props.get("format.dsp");
|
||||
let audio_channel = props.get("audio.channel");
|
||||
let port_id = props.get("port.id");
|
||||
let port_direction = props.get("port.direction");
|
||||
println!("Port: Name: {:?} Alias: {:?} Id: {:?} Direction: {:?} AudioChannel: {:?} Object Path: {:?} FormatDsp: {:?}",
|
||||
port_name,
|
||||
port_alias,
|
||||
port_id,port_direction,audio_channel,object_path,format_dsp
|
||||
);
|
||||
} else if global.type_ == ObjectType::Device {
|
||||
let props = global.props.as_ref().unwrap();
|
||||
let device_name = props.get("device.name");
|
||||
let device_nick = props.get("device.nick");
|
||||
let device_description = props.get("device.description");
|
||||
let device_api = props.get("device.api");
|
||||
let media_class = props.get("media.class");
|
||||
println!("Device: Name: {:?} Nick: {:?} Desc: {:?} Api: {:?} MediaClass: {:?}",
|
||||
device_name, device_nick, device_description, device_api, media_class);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
)
|
||||
.register();
|
||||
|
||||
|
||||
|
||||
let _receiver = rx.attach(mainloop.loop_(), {
|
||||
let mainloop = mainloop.clone();
|
||||
{
|
||||
let globals_manager_weak = Rc::downgrade(&globals_manager);
|
||||
move |command| {
|
||||
let Some(globals_manager) = globals_manager_weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match command {
|
||||
Command::GetDeviceNames => {
|
||||
let device_names = globals_manager.borrow().get_device_names();
|
||||
tx.send(Data::DeviceNames(device_names)).unwrap();
|
||||
}
|
||||
Command::GetDeviceProperties(id) => {
|
||||
let device_properties = globals_manager.borrow().get_device_properties(id);
|
||||
tx.send(Data::DeviceProperties(id, device_properties)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calling the `destroy_global` method on the registry will destroy the object with the specified id on the remote.
|
||||
// We don't have a specific object to destroy now, so this is commented out.
|
||||
// registry.destroy_global(313).into_result()?;
|
||||
|
||||
|
||||
mainloop.run();
|
||||
return ExitCode::FAILURE; // not sure
|
||||
});
|
||||
|
||||
(pw_thread, remote_tx, remote_rx)
|
||||
}
|
||||
|
||||
mod deserialize {
|
||||
/// Taken from wiremix: https://github.com/tsowell/wiremix/blob/main/src/wirehose/deserialize.rs#L6
|
||||
use libspa::pod::{deserialize::PodDeserializer, Object, Pod, Value};
|
||||
pub fn deserialize(param: Option<&Pod>) -> Option<Object> {
|
||||
param
|
||||
.and_then(|pod| {
|
||||
PodDeserializer::deserialize_any_from(pod.as_bytes()).ok()
|
||||
})
|
||||
.and_then(|(_, value)| match value {
|
||||
Value::Object(obj) => Some(obj),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
151
src/spa_structs.rs
Normal file
151
src/spa_structs.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use libspa_sys::{SPA_AUDIO_CHANNEL_START_Aux, SPA_AUDIO_CHANNEL_START_Custom, SPA_AUDIO_CHANNEL_BC, SPA_AUDIO_CHANNEL_BLC, SPA_AUDIO_CHANNEL_BRC, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_FCH, SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FLC, SPA_AUDIO_CHANNEL_FLH, SPA_AUDIO_CHANNEL_FLW, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FRC, SPA_AUDIO_CHANNEL_FRH, SPA_AUDIO_CHANNEL_FRW, SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_LFE2, SPA_AUDIO_CHANNEL_LLFE, SPA_AUDIO_CHANNEL_MONO, SPA_AUDIO_CHANNEL_NA, SPA_AUDIO_CHANNEL_RC, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RLC, SPA_AUDIO_CHANNEL_RLFE, SPA_AUDIO_CHANNEL_RR, SPA_AUDIO_CHANNEL_RRC, SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, SPA_AUDIO_CHANNEL_TC, SPA_AUDIO_CHANNEL_TFC, SPA_AUDIO_CHANNEL_TFL, SPA_AUDIO_CHANNEL_TFLC, SPA_AUDIO_CHANNEL_TFR, SPA_AUDIO_CHANNEL_TFRC, SPA_AUDIO_CHANNEL_TRC, SPA_AUDIO_CHANNEL_TRL, SPA_AUDIO_CHANNEL_TRR, SPA_AUDIO_CHANNEL_TSL, SPA_AUDIO_CHANNEL_TSR, SPA_AUDIO_CHANNEL_UNKNOWN};
|
||||
|
||||
// Modified from https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/spa/include/spa/param/audio/raw.h
|
||||
pub(crate) enum Channel {
|
||||
UNKNOWN, /**< unspecified */
|
||||
NA, /**< N/A, silent */
|
||||
|
||||
MONO, /**< mono stream */
|
||||
|
||||
FL, /**< front left */
|
||||
FR, /**< front right */
|
||||
FC, /**< front center */
|
||||
LFE, /**< LFE */
|
||||
SL, /**< side left */
|
||||
SR, /**< side right */
|
||||
FLC, /**< front left center */
|
||||
FRC, /**< front right center */
|
||||
RC, /**< rear center */
|
||||
RL, /**< rear left */
|
||||
RR, /**< rear right */
|
||||
TC, /**< top center */
|
||||
TFL, /**< top front left */
|
||||
TFC, /**< top front center */
|
||||
TFR, /**< top front right */
|
||||
TRL, /**< top rear left */
|
||||
TRC, /**< top rear center */
|
||||
TRR, /**< top rear right */
|
||||
RLC, /**< rear left center */
|
||||
RRC, /**< rear right center */
|
||||
FLW, /**< front left wide */
|
||||
FRW, /**< front right wide */
|
||||
LFE2, /**< LFE 2 */
|
||||
FLH, /**< front left high */
|
||||
FCH, /**< front center high */
|
||||
FRH, /**< front right high */
|
||||
TFLC, /**< top front left center */
|
||||
TFRC, /**< top front right center */
|
||||
TSL, /**< top side left */
|
||||
TSR, /**< top side right */
|
||||
LLFE, /**< left LFE */
|
||||
RLFE, /**< right LFE */
|
||||
BC, /**< bottom center */
|
||||
BLC, /**< bottom left center */
|
||||
BRC, /**< bottom right center */
|
||||
AUX(u32), // 0 - 4095
|
||||
CUSTOM(u32),
|
||||
}
|
||||
|
||||
impl From<Channel> for libspa_sys::spa_audio_channel {
|
||||
fn from(value: Channel) -> Self {
|
||||
match value {
|
||||
Channel::UNKNOWN => SPA_AUDIO_CHANNEL_UNKNOWN,
|
||||
Channel::NA => SPA_AUDIO_CHANNEL_NA,
|
||||
Channel::MONO => SPA_AUDIO_CHANNEL_MONO,
|
||||
Channel::FL => SPA_AUDIO_CHANNEL_FL,
|
||||
Channel::FR => SPA_AUDIO_CHANNEL_FR,
|
||||
Channel::FC => SPA_AUDIO_CHANNEL_FC,
|
||||
Channel::LFE => SPA_AUDIO_CHANNEL_LFE,
|
||||
Channel::SL => SPA_AUDIO_CHANNEL_SL,
|
||||
Channel::SR => SPA_AUDIO_CHANNEL_SR,
|
||||
Channel::FLC => SPA_AUDIO_CHANNEL_FLC,
|
||||
Channel::FRC => SPA_AUDIO_CHANNEL_FRC,
|
||||
Channel::RC => SPA_AUDIO_CHANNEL_RC,
|
||||
Channel::RL => SPA_AUDIO_CHANNEL_RL,
|
||||
Channel::RR => SPA_AUDIO_CHANNEL_RR,
|
||||
Channel::TC => SPA_AUDIO_CHANNEL_TC,
|
||||
Channel::TFL => SPA_AUDIO_CHANNEL_TFL,
|
||||
Channel::TFC => SPA_AUDIO_CHANNEL_TFC,
|
||||
Channel::TFR => SPA_AUDIO_CHANNEL_TFR,
|
||||
Channel::TRL => SPA_AUDIO_CHANNEL_TRL,
|
||||
Channel::TRC => SPA_AUDIO_CHANNEL_TRC,
|
||||
Channel::TRR => SPA_AUDIO_CHANNEL_TRR,
|
||||
Channel::RLC => SPA_AUDIO_CHANNEL_RLC,
|
||||
Channel::RRC => SPA_AUDIO_CHANNEL_RRC,
|
||||
Channel::FLW => SPA_AUDIO_CHANNEL_FLW,
|
||||
Channel::FRW => SPA_AUDIO_CHANNEL_FRW,
|
||||
Channel::LFE2 => SPA_AUDIO_CHANNEL_LFE2,
|
||||
Channel::FLH => SPA_AUDIO_CHANNEL_FLH,
|
||||
Channel::FCH => SPA_AUDIO_CHANNEL_FCH,
|
||||
Channel::FRH => SPA_AUDIO_CHANNEL_FRH,
|
||||
Channel::TFLC => SPA_AUDIO_CHANNEL_TFLC,
|
||||
Channel::TFRC => SPA_AUDIO_CHANNEL_TFRC,
|
||||
Channel::TSL => SPA_AUDIO_CHANNEL_TSL,
|
||||
Channel::TSR => SPA_AUDIO_CHANNEL_TSR,
|
||||
Channel::LLFE => SPA_AUDIO_CHANNEL_LLFE,
|
||||
Channel::RLFE => SPA_AUDIO_CHANNEL_RLFE,
|
||||
Channel::BC => SPA_AUDIO_CHANNEL_BC,
|
||||
Channel::BLC => SPA_AUDIO_CHANNEL_BLC,
|
||||
Channel::BRC => SPA_AUDIO_CHANNEL_BRC,
|
||||
Channel::AUX(aux) => SPA_AUDIO_CHANNEL_START_Aux + aux,
|
||||
Channel::CUSTOM(custom) => SPA_AUDIO_CHANNEL_START_Custom + custom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/spa/include/spa/param/audio/raw-types.h
|
||||
impl From<Channel> for String {
|
||||
fn from(value: Channel) -> String {
|
||||
match value {
|
||||
Channel::UNKNOWN => "UNK".to_owned(),
|
||||
Channel::NA => "NA".to_owned(),
|
||||
Channel::MONO => "MONO".to_owned(),
|
||||
Channel::FL => "FL".to_owned(),
|
||||
Channel::FR => "FR".to_owned(),
|
||||
Channel::FC => "FC".to_owned(),
|
||||
Channel::LFE => "LFE".to_owned(),
|
||||
Channel::SL => "SL".to_owned(),
|
||||
Channel::SR => "SR".to_owned(),
|
||||
Channel::FLC => "FLC".to_owned(),
|
||||
Channel::FRC => "FRC".to_owned(),
|
||||
Channel::RC => "RC".to_owned(),
|
||||
Channel::RL => "RL".to_owned(),
|
||||
Channel::RR => "RR".to_owned(),
|
||||
Channel::TC => "TC".to_owned(),
|
||||
Channel::TFL => "TFL".to_owned(),
|
||||
Channel::TFC => "TFC".to_owned(),
|
||||
Channel::TFR => "TFR".to_owned(),
|
||||
Channel::TRL => "TRL".to_owned(),
|
||||
Channel::TRC => "TRC".to_owned(),
|
||||
Channel::TRR => "TRR".to_owned(),
|
||||
Channel::RLC => "RLC".to_owned(),
|
||||
Channel::RRC => "RRC".to_owned(),
|
||||
Channel::FLW => "FLW".to_owned(),
|
||||
Channel::FRW => "FRW".to_owned(),
|
||||
Channel::LFE2 => "LFE2".to_owned(),
|
||||
Channel::FLH => "FLH".to_owned(),
|
||||
Channel::FCH => "FCH".to_owned(),
|
||||
Channel::FRH => "FRH".to_owned(),
|
||||
Channel::TFLC => "TFLC".to_owned(),
|
||||
Channel::TFRC => "TFRC".to_owned(),
|
||||
Channel::TSL => "TSL".to_owned(),
|
||||
Channel::TSR => "TSR".to_owned(),
|
||||
Channel::LLFE => "LLFE".to_owned(),
|
||||
Channel::RLFE => "RLFE".to_owned(),
|
||||
Channel::BC => "BC".to_owned(),
|
||||
Channel::BLC => "BLC".to_owned(),
|
||||
Channel::BRC => "BRC".to_owned(),
|
||||
Channel::AUX(aux) => {
|
||||
// > 63 is not official spec
|
||||
if aux > 4095 {
|
||||
panic!("AUX Channel has to be in range 0 - 4095");
|
||||
}
|
||||
format!("AUX{}", aux)
|
||||
},
|
||||
Channel::CUSTOM(custom) => {
|
||||
// not official spec
|
||||
format!("CUSTOM{}", custom)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
use adw::prelude::*;
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use adw::gdk::Device;
|
||||
use uuid::Uuid;
|
||||
use crate::state_manager::simple_serde::SimpleSerde;
|
||||
use crate::state_manager::structs::DeviceConfig;
|
||||
|
||||
@ -80,10 +82,13 @@ pub(crate) mod backends {
|
||||
|
||||
|
||||
pub(crate) mod structs {
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use uuid::{Timestamp, Uuid};
|
||||
use crate::spa_structs::Channel;
|
||||
use crate::state_manager::StateBackend;
|
||||
|
||||
pub(crate) struct DeviceConfig {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
identifying_properties: HashMap<String, String>,
|
||||
available_profiles: Vec<String>,
|
||||
@ -99,7 +104,10 @@ pub(crate) mod structs {
|
||||
backend: &mut Backend,
|
||||
namespace: &Option<String>,
|
||||
) -> Self {
|
||||
let id = Uuid::now_v7();
|
||||
|
||||
let instance = Self {
|
||||
id,
|
||||
name,
|
||||
identifying_properties,
|
||||
available_profiles,
|
||||
@ -110,15 +118,26 @@ pub(crate) mod structs {
|
||||
|
||||
instance
|
||||
}
|
||||
pub(super) fn load<Backend: StateBackend>(name: String, backend: &mut Backend, namespace: &Option<String>) -> Self {
|
||||
|
||||
fn get_namespaces(id: Uuid, namespace: &Option<String>) -> (Option<String>, Option<String>) {
|
||||
let device_namespace_string = match namespace {
|
||||
Some(ns) => ns.clone() + "/" + &name,
|
||||
None => name.clone()
|
||||
Some(ns) => ns.clone() + "/" + &id.to_string(),
|
||||
None => id.to_string()
|
||||
};
|
||||
|
||||
let properties_namespace = Some(device_namespace_string.clone() + "/" + "identifying_properties");
|
||||
let device_namespace = Some(device_namespace_string);
|
||||
|
||||
(
|
||||
device_namespace,
|
||||
properties_namespace,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn load<Backend: StateBackend>(id: Uuid, backend: &mut Backend, namespace: &Option<String>) -> Self {
|
||||
|
||||
let (device_namespace, properties_namespace) = Self::get_namespaces(id, namespace);
|
||||
|
||||
let properties = backend.get("properties", &device_namespace).unwrap_or(Vec::new());
|
||||
|
||||
let identifying_properties = properties.into_iter()
|
||||
@ -129,32 +148,28 @@ pub(crate) mod structs {
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
name,
|
||||
id,
|
||||
name: backend.get("name", &device_namespace).expect("name not found"),
|
||||
available_profiles: backend.get("available_profiles", &device_namespace).expect("available_profiles not found"),
|
||||
selected_profile: backend.get("selected_profile", &device_namespace),
|
||||
identifying_properties
|
||||
}
|
||||
}
|
||||
pub(super) fn save<Backend: StateBackend>(&self, backend: &mut Backend, namespace: &Option<String>) -> Result<(), String> {
|
||||
let mut devices: Vec<String> = backend.get("devices", &namespace)
|
||||
let mut devices: Vec<Uuid> = backend.get("devices", &namespace)
|
||||
.unwrap_or(Vec::new());
|
||||
|
||||
if devices.contains(&self.name) {
|
||||
return Err("Two devices with the same name cannot exist".to_string())
|
||||
if devices.contains(&self.id) {
|
||||
Self::remove_by_id(self.id, backend, namespace);
|
||||
} else {
|
||||
devices.push(self.id);
|
||||
}
|
||||
|
||||
devices.push(self.name.clone());
|
||||
|
||||
backend.insert("devices", &devices, &namespace);
|
||||
|
||||
let device_namespace_string = match namespace {
|
||||
Some(ns) => ns.clone() + "/" + &self.name,
|
||||
None => self.name.clone()
|
||||
};
|
||||
|
||||
let properties_namespace = Some(device_namespace_string.clone() + "/" + "identifying_properties");
|
||||
let device_namespace = Some(device_namespace_string);
|
||||
let (device_namespace, properties_namespace) = Self::get_namespaces(self.id, namespace);
|
||||
|
||||
backend.insert("name", &self.name, &device_namespace);
|
||||
backend.insert("available_profiles", &self.available_profiles, &device_namespace);
|
||||
backend.insert_option("selected_profile", &self.selected_profile, &device_namespace);
|
||||
|
||||
@ -171,10 +186,50 @@ pub(crate) mod structs {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn remove_by_id<Backend: StateBackend>(id: Uuid, backend: &mut Backend, namespace: &Option<String>) {
|
||||
let mut devices: Vec<Uuid> = backend.get("devices", &namespace)
|
||||
.unwrap_or(Vec::new());
|
||||
|
||||
devices = devices.into_iter()
|
||||
.filter(|device_id| *device_id != id)
|
||||
.collect();
|
||||
|
||||
backend.insert("devices", &devices, &namespace);
|
||||
|
||||
let (device_namespace, properties_namespace) = Self::get_namespaces(id, namespace);
|
||||
|
||||
backend.remove("name", &device_namespace);
|
||||
backend.remove("available_profiles", &device_namespace);
|
||||
backend.remove("selected_profile", &device_namespace);
|
||||
|
||||
let properties: Vec<String> = backend.get("properties", &device_namespace).unwrap_or(Vec::new());
|
||||
|
||||
backend.remove("properties", &device_namespace);
|
||||
|
||||
for name in properties {
|
||||
backend.remove(&name, &properties_namespace);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn remove<Backend: StateBackend>(&self, backend: &mut Backend, namespace: &Option<String>) {
|
||||
Self::remove_by_id(self.id, backend, namespace);
|
||||
}
|
||||
|
||||
pub(super) fn change_name<Backend: StateBackend>(&mut self, new_name: String, backend: &mut Backend, namespace: &Option<String>) {
|
||||
|
||||
let (device_namespace, _) = Self::get_namespaces(self.id, namespace);
|
||||
|
||||
backend.insert("name", &new_name, &device_namespace);
|
||||
|
||||
self.name = new_name;
|
||||
}
|
||||
|
||||
pub(crate) fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub(crate) fn get_id(&self) -> Uuid { self.id }
|
||||
|
||||
pub(crate) fn get_identifying_properties(&self) -> &HashMap<String, String> {
|
||||
&self.identifying_properties
|
||||
}
|
||||
@ -186,11 +241,64 @@ pub(crate) mod structs {
|
||||
pub(crate) fn get_selected_profile(&self) -> &Option<String> {
|
||||
&self.selected_profile
|
||||
}
|
||||
|
||||
pub(crate) fn get_available_channels(&self) -> HashSet<Channel> {
|
||||
let channels = HashSet::new();
|
||||
|
||||
channels
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InputProfile {
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
pub(crate) struct OutputProfile {
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
enum LoopbackProfile {
|
||||
Input(Uuid),
|
||||
Output(Uuid),
|
||||
}
|
||||
|
||||
enum LoopbackSource {
|
||||
DeviceConfig(Uuid),
|
||||
Loopback(Uuid)
|
||||
}
|
||||
|
||||
pub(crate) struct Loopback {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
profile: LoopbackProfile,
|
||||
virtual_only: bool,
|
||||
// has to be checked for validity everytime it's loaded
|
||||
// If Source changes profile, available channels also change
|
||||
// Perhaps backreference, so that warning can be given?
|
||||
mappings: HashMap<Channel, (LoopbackSource, String)>
|
||||
|
||||
}
|
||||
pub(crate) struct Output {
|
||||
name: String,
|
||||
output_profile: Uuid,
|
||||
}
|
||||
|
||||
pub(crate) struct Device {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
device_configs: Vec<Uuid>,
|
||||
inputs: Vec<Loopback>,
|
||||
outputs: Vec<Loopback>,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod simple_serde {
|
||||
use std::fmt::Debug;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) trait SimpleSerde {
|
||||
fn serialize(&self) -> Vec<u8>;
|
||||
@ -266,10 +374,22 @@ pub(crate) mod simple_serde {
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleSerde for Uuid {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
self.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn deserialize(bytes: Vec<u8>) -> Self {
|
||||
let uuid_bytes = uuid::Bytes::try_from(bytes).unwrap();
|
||||
Uuid::from_bytes(uuid_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RootNamespaces {
|
||||
None,
|
||||
Devices,
|
||||
DeviceConfigs,
|
||||
}
|
||||
|
||||
@ -277,59 +397,78 @@ impl From<RootNamespaces> for Option<String> {
|
||||
fn from(value: RootNamespaces) -> Self {
|
||||
match value {
|
||||
RootNamespaces::None => None,
|
||||
RootNamespaces::DeviceConfigs => Some("DeviceConfigs".to_string()),
|
||||
RootNamespaces::Devices => Some("Devices".to_owned()),
|
||||
RootNamespaces::DeviceConfigs => Some("DeviceConfigs".to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct StateManager<Backend: StateBackend> {
|
||||
db: Backend,
|
||||
device_configs: HashMap<String, DeviceConfig>
|
||||
selected_device: String,
|
||||
device_configs: HashMap<Uuid, DeviceConfig>,
|
||||
device_config_action_rows: HashMap<String, HashMap<Uuid, Vec<adw::ActionRow>>>
|
||||
}
|
||||
|
||||
impl<Backend: StateBackend> StateManager<Backend> {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut instance = Self {
|
||||
db: Backend::new(crate::utils::CONFIG_DIR.clone().join("db")),
|
||||
selected_device: String::new(),
|
||||
device_configs: HashMap::new(),
|
||||
device_config_action_rows: HashMap::new()
|
||||
};
|
||||
instance.load();
|
||||
instance
|
||||
}
|
||||
|
||||
pub(crate) fn load(&mut self) {
|
||||
self.selected_device = self.db.get("selected_device", &RootNamespaces::Devices.into())
|
||||
.unwrap_or("".to_owned());
|
||||
|
||||
let device_config_names: Vec<String> = self.db.get("devices", &RootNamespaces::DeviceConfigs.into())
|
||||
let device_config_ids: Vec<Uuid> = self.db.get("devices", &RootNamespaces::DeviceConfigs.into())
|
||||
.unwrap_or(Vec::new());
|
||||
|
||||
println!("device_config_names: {:?}", device_config_names);
|
||||
println!("device_config_ids: {:?}", device_config_ids);
|
||||
|
||||
for device_config_name in device_config_names {
|
||||
let device_config = DeviceConfig::load(device_config_name.clone(), &mut self.db, &RootNamespaces::DeviceConfigs.into());
|
||||
self.device_configs.insert(device_config_name, device_config);
|
||||
for id in device_config_ids {
|
||||
let device_config = DeviceConfig::load(id, &mut self.db, &RootNamespaces::DeviceConfigs.into());
|
||||
self.device_configs.insert(id, device_config);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_device_config_names(&mut self) -> Vec<String> {
|
||||
pub(crate) fn get_device_config_ids(&self) -> Vec<Uuid> {
|
||||
self.device_configs.iter()
|
||||
.map(|(name, _)| name.clone())
|
||||
.map(|(id, _)| *id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get_device_config_properties(&mut self, name: &str) -> &HashMap<String, String> {
|
||||
self.device_configs.get(name)
|
||||
pub(crate) fn get_device_config_name(&self, device_config_id: &Uuid) -> String {
|
||||
self.device_configs.get(device_config_id).unwrap()
|
||||
.get_name()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn get_device_config_names(&self) -> HashMap<Uuid, String> {
|
||||
self.device_configs.iter()
|
||||
.map(|(id, config)| (*id, config.get_name().to_owned()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get_device_config_properties(&self, device_config_id: &Uuid) -> &HashMap<String, String> {
|
||||
self.device_configs.get(device_config_id)
|
||||
.unwrap()
|
||||
.get_identifying_properties()
|
||||
}
|
||||
|
||||
pub(crate) fn get_device_config_profiles(&mut self, name: &str) -> &Vec<String> {
|
||||
self.device_configs.get(name)
|
||||
pub(crate) fn get_device_config_profiles(&self, device_config_id: &Uuid) -> &Vec<String> {
|
||||
self.device_configs.get(device_config_id)
|
||||
.unwrap()
|
||||
.get_available_profiles()
|
||||
}
|
||||
|
||||
pub(crate) fn get_device_config_selected_profile(&mut self, name: &str) -> &Option<String> {
|
||||
self.device_configs.get(name)
|
||||
pub(crate) fn get_device_config_selected_profile(&self, device_config_id: &Uuid) -> &Option<String> {
|
||||
self.device_configs.get(device_config_id)
|
||||
.unwrap()
|
||||
.get_selected_profile()
|
||||
}
|
||||
@ -340,7 +479,7 @@ impl<Backend: StateBackend> StateManager<Backend> {
|
||||
identifying_properties: HashMap<String, String>,
|
||||
profiles: Vec<String>,
|
||||
active_profile: Option<String>
|
||||
) {
|
||||
) -> Uuid {
|
||||
let config = DeviceConfig::new(
|
||||
name.clone(),
|
||||
identifying_properties,
|
||||
@ -350,7 +489,76 @@ impl<Backend: StateBackend> StateManager<Backend> {
|
||||
&RootNamespaces::DeviceConfigs.into()
|
||||
);
|
||||
|
||||
self.device_configs.insert(name, config);
|
||||
let config_id = config.get_id();
|
||||
|
||||
self.device_configs.insert(config_id, config);
|
||||
|
||||
config_id
|
||||
}
|
||||
|
||||
// Register action_rows, so that if the device config name is changed
|
||||
// the action row text is changed too
|
||||
pub(crate) fn register_device_config_action_row(
|
||||
&mut self,
|
||||
device_name: String,
|
||||
device_config_id: Uuid,
|
||||
row: adw::ActionRow
|
||||
) {
|
||||
row.set_title(
|
||||
&self.get_device_config_name(&device_config_id)
|
||||
);
|
||||
|
||||
self.device_config_action_rows
|
||||
.entry(device_name).or_insert(HashMap::new())
|
||||
.entry(device_config_id).or_insert(vec![])
|
||||
.push(row);
|
||||
}
|
||||
|
||||
pub(crate) fn change_device_config_name(
|
||||
&mut self,
|
||||
device_config_id: &Uuid,
|
||||
new_name: String,
|
||||
) {
|
||||
let config = self.device_configs.get_mut(device_config_id).unwrap();
|
||||
|
||||
config
|
||||
.change_name(
|
||||
new_name.clone(),
|
||||
&mut self.db,
|
||||
&RootNamespaces::DeviceConfigs.into()
|
||||
);
|
||||
|
||||
for (_, rows_map) in self.device_config_action_rows.iter_mut() {
|
||||
rows_map.get(device_config_id)
|
||||
.map(|rows| {
|
||||
for row in rows {
|
||||
row.set_title(&new_name);
|
||||
};
|
||||
rows
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Unregister device, so that after use all widgets registered with it
|
||||
// can be freed up again
|
||||
pub(crate) fn unregister_device(
|
||||
&mut self,
|
||||
device_name: &str,
|
||||
) {
|
||||
self.device_config_action_rows.remove(device_name);
|
||||
}
|
||||
|
||||
pub(crate) fn set_current_device(
|
||||
&mut self,
|
||||
device_name: String,
|
||||
) {
|
||||
self.db.insert("selected_device", &device_name, &RootNamespaces::Devices.into());
|
||||
self.selected_device = device_name;
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_device(&self) -> &String {
|
||||
&self.selected_device
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
src/window/message_handler.rs
Normal file
5
src/window/message_handler.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use crate::window::AudioDeviceManagerWindow;
|
||||
|
||||
async fn message_handler(main_window: &AudioDeviceManagerWindow) {
|
||||
|
||||
}
|
||||
@ -20,6 +20,7 @@
|
||||
*/
|
||||
mod popups;
|
||||
mod subpages;
|
||||
mod message_handler;
|
||||
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::cmp::min;
|
||||
@ -31,6 +32,7 @@ use gtk::prelude::*;
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{gio, glib};
|
||||
use uuid::Uuid;
|
||||
use subpages::device_config;
|
||||
|
||||
#[derive(Default)]
|
||||
@ -60,7 +62,7 @@ impl Default for RcProceduralChildManager {
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use crate::pw_manager::PipewireManager;
|
||||
use crate::pipewire_manager::PipewireManager;
|
||||
use crate::state_manager::RcStateManager;
|
||||
use crate::state_manager::backends::SledBackend;
|
||||
use super::*;
|
||||
@ -202,7 +204,7 @@ impl AudioDeviceManagerWindow {
|
||||
.child()
|
||||
.and_downcast()
|
||||
.expect("No Label in Row");
|
||||
window.select_device(label.text().to_string());
|
||||
window.select_device(label.text().to_owned());
|
||||
}
|
||||
));
|
||||
*/
|
||||
@ -216,24 +218,30 @@ impl AudioDeviceManagerWindow {
|
||||
*/
|
||||
}
|
||||
|
||||
fn add_device_config(&self, config_name: &str) {
|
||||
fn add_device_config(&self, config_id: Uuid) {
|
||||
self.imp().procedural_child_manager.borrow_mut().device_configs_preferences_group.add(
|
||||
&self.build_device_config_row(config_name.to_string())
|
||||
&self.build_device_config_row(config_id)
|
||||
);
|
||||
}
|
||||
|
||||
fn build_device_config_row(&self, config_name: String) -> adw::ActionRow {
|
||||
fn build_device_config_row(&self, config_id: Uuid) -> adw::ActionRow {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(config_name)
|
||||
.activatable(true)
|
||||
.build();
|
||||
|
||||
self.imp().state_manager.borrow_mut().register_device_config_action_row(
|
||||
String::from(self.imp().device_navigation_page.title()),
|
||||
config_id,
|
||||
row.clone()
|
||||
);
|
||||
|
||||
row.connect_activated(
|
||||
clone!(
|
||||
#[weak(rename_to = window)]
|
||||
self,
|
||||
move |row| {
|
||||
println!("Row selected {:?}", row);
|
||||
let device_name = String::from(row.title());
|
||||
device_config::switch_to_device_config_window(&window, config_id);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
89
src/window/popups/change_device_config_name.rs
Normal file
89
src/window/popups/change_device_config_name.rs
Normal file
@ -0,0 +1,89 @@
|
||||
use super::*;
|
||||
use adw::ResponseAppearance;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(in crate::window) async fn change_device_config_name(
|
||||
main_window: &AudioDeviceManagerWindow,
|
||||
device_config_id: Uuid,
|
||||
) -> Option<String> {
|
||||
let old_name = main_window.imp().state_manager.borrow().get_device_config_name(&device_config_id);
|
||||
|
||||
let entry = gtk::Entry::builder()
|
||||
.placeholder_text("Name")
|
||||
.text(old_name.clone())
|
||||
.activates_default(true)
|
||||
.build();
|
||||
|
||||
let cancel_response = "cancel";
|
||||
let change_response = "change";
|
||||
|
||||
// Create new dialog
|
||||
let dialog = adw::AlertDialog::builder()
|
||||
.heading("Change Configuration Name")
|
||||
.close_response(cancel_response)
|
||||
.default_response(change_response)
|
||||
.extra_child(&entry)
|
||||
.build();
|
||||
|
||||
dialog.add_responses(&[(cancel_response, "Cancel"), (change_response, "Change")]);
|
||||
|
||||
dialog.set_response_appearance(change_response, ResponseAppearance::Suggested);
|
||||
|
||||
let config_names: Vec<String> = main_window.imp().state_manager.borrow_mut()
|
||||
.get_device_config_names()
|
||||
.into_values()
|
||||
.collect();
|
||||
|
||||
// Set entry's css class to "error", when there is no text in it
|
||||
entry.connect_changed(clone!(
|
||||
#[weak]
|
||||
dialog,
|
||||
#[strong]
|
||||
old_name,
|
||||
move |entry| {
|
||||
let text = entry.text();
|
||||
let empty = text.is_empty();
|
||||
let exists = config_names.contains(&String::from(text.clone()));
|
||||
|
||||
let mut err = empty || exists;
|
||||
|
||||
if text == old_name {
|
||||
err = false;
|
||||
}
|
||||
|
||||
dialog.set_response_enabled(change_response, !err);
|
||||
|
||||
if err {
|
||||
entry.add_css_class("error");
|
||||
} else {
|
||||
entry.remove_css_class("error");
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
let response = dialog.choose_future(main_window).await;
|
||||
|
||||
// Return if the user chose 'cancel_response'
|
||||
|
||||
if response == cancel_response {
|
||||
println!("Cancel");
|
||||
return None;
|
||||
};
|
||||
|
||||
if response == change_response {
|
||||
let new_name = entry.text().to_string();
|
||||
|
||||
if old_name == new_name {
|
||||
return None;
|
||||
}
|
||||
|
||||
main_window.imp().state_manager.borrow_mut().change_device_config_name(
|
||||
&device_config_id,
|
||||
new_name.clone()
|
||||
);
|
||||
|
||||
return Some(new_name);
|
||||
};
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
@ -7,6 +7,8 @@ use crate::window::AudioDeviceManagerWindow;
|
||||
|
||||
pub(super) mod new_device;
|
||||
pub(super) mod select_device_config;
|
||||
mod new_device_config;
|
||||
pub(super) mod new_device_config;
|
||||
pub mod change_device_config_name;
|
||||
mod new_device_config_type;
|
||||
mod select_pipewire_device;
|
||||
mod new_device_config_name;
|
||||
@ -1,51 +1,25 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::window::popups::new_device_config_name::new_device_config_name;
|
||||
use crate::window::subpages;
|
||||
use super::*;
|
||||
use adw::ResponseAppearance;
|
||||
use crate::window::popups;
|
||||
|
||||
pub(in crate::window) async fn new_device_config(main_window: &AudioDeviceManagerWindow) {
|
||||
let cancel_response = "cancel";
|
||||
let create_from_template_response = "create-from-template";
|
||||
let create_new_response = "create-new";
|
||||
pub(in crate::window) async fn new_device_config(
|
||||
main_window: &AudioDeviceManagerWindow,
|
||||
name_suggestion: Option<String>,
|
||||
properties: HashMap<String, String>,
|
||||
profiles: Vec<String>,
|
||||
selected_profile: Option<String>,
|
||||
|
||||
// Create new dialog
|
||||
let dialog = adw::AlertDialog::builder()
|
||||
.heading("New Configuration")
|
||||
.close_response(cancel_response)
|
||||
.default_response(create_from_template_response)
|
||||
.build();
|
||||
) {
|
||||
let Some(name) = new_device_config_name(main_window, name_suggestion).await else { return; };
|
||||
|
||||
dialog.add_responses(&[
|
||||
(cancel_response, "Cancel"),
|
||||
(create_new_response, "Create From Scratch"),
|
||||
(create_from_template_response, "Use Pipewire Device as Template"),
|
||||
]);
|
||||
let config_id = main_window.imp().state_manager.borrow_mut().create_new_device_config(
|
||||
name.clone(),
|
||||
properties,
|
||||
profiles,
|
||||
selected_profile
|
||||
);
|
||||
|
||||
// Make the dialog button insensitive initially
|
||||
dialog.set_response_appearance(create_from_template_response, ResponseAppearance::Suggested);
|
||||
dialog.set_response_appearance(cancel_response, ResponseAppearance::Destructive);
|
||||
|
||||
let response = dialog.choose_future(main_window).await;
|
||||
|
||||
// Return if the user chose 'cancel_response'
|
||||
if response == cancel_response {
|
||||
println!("Cancel");
|
||||
return;
|
||||
};
|
||||
|
||||
if response == create_from_template_response {
|
||||
popups::select_pipewire_device::select_pipewire_device(main_window).await;
|
||||
return;
|
||||
};
|
||||
|
||||
if response == create_new_response {
|
||||
popups::new_device_config_name::new_device_config_name(
|
||||
main_window,
|
||||
None,
|
||||
HashMap::new(),
|
||||
Vec::new(),
|
||||
None
|
||||
).await;
|
||||
return;
|
||||
}
|
||||
main_window.add_device_config(config_id);
|
||||
subpages::device_config::switch_to_device_config_window(main_window, config_id);
|
||||
}
|
||||
@ -8,10 +8,7 @@ use super::*;
|
||||
pub(in crate::window) async fn new_device_config_name(
|
||||
main_window: &AudioDeviceManagerWindow,
|
||||
name_suggestion: Option<String>,
|
||||
properties: HashMap<String, String>,
|
||||
profiles: Vec<String>,
|
||||
selected_profile: Option<String>,
|
||||
) {
|
||||
) -> Option<String> {
|
||||
let entry = gtk::Entry::builder()
|
||||
.placeholder_text("Name")
|
||||
.activates_default(true)
|
||||
@ -34,7 +31,10 @@ pub(in crate::window) async fn new_device_config_name(
|
||||
dialog.set_response_enabled(create_response, false);
|
||||
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
|
||||
|
||||
let config_names = main_window.imp().state_manager.borrow_mut().get_device_config_names();
|
||||
let config_names: Vec<String> = main_window.imp().state_manager.borrow_mut()
|
||||
.get_device_config_names()
|
||||
.into_values()
|
||||
.collect();
|
||||
|
||||
if let Some(init_name) = name_suggestion {
|
||||
entry.set_text(&init_name);
|
||||
@ -72,22 +72,14 @@ pub(in crate::window) async fn new_device_config_name(
|
||||
|
||||
if response == cancel_response {
|
||||
println!("Cancel");
|
||||
return;
|
||||
return None;
|
||||
};
|
||||
|
||||
if response == create_response {
|
||||
let device_config_name = entry.text().to_string();
|
||||
|
||||
main_window.imp().state_manager.borrow_mut().create_new_device_config(
|
||||
device_config_name.clone(),
|
||||
properties,
|
||||
profiles,
|
||||
selected_profile
|
||||
);
|
||||
|
||||
main_window.add_device_config(&device_config_name);
|
||||
subpages::device_config::switch_to_device_config_window(main_window, device_config_name);
|
||||
return;
|
||||
return Some(device_config_name);
|
||||
};
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
51
src/window/popups/new_device_config_type.rs
Normal file
51
src/window/popups/new_device_config_type.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use std::collections::HashMap;
|
||||
use super::*;
|
||||
use adw::ResponseAppearance;
|
||||
use crate::window::popups;
|
||||
|
||||
pub(in crate::window) async fn new_device_config_type(main_window: &AudioDeviceManagerWindow) {
|
||||
let cancel_response = "cancel";
|
||||
let create_from_template_response = "create-from-template";
|
||||
let create_new_response = "create-new";
|
||||
|
||||
// Create new dialog
|
||||
let dialog = adw::AlertDialog::builder()
|
||||
.heading("New Configuration")
|
||||
.close_response(cancel_response)
|
||||
.default_response(create_from_template_response)
|
||||
.build();
|
||||
|
||||
dialog.add_responses(&[
|
||||
(cancel_response, "Cancel"),
|
||||
(create_new_response, "Create From Scratch"),
|
||||
(create_from_template_response, "Use Pipewire Device as Template"),
|
||||
]);
|
||||
|
||||
// Make the dialog button insensitive initially
|
||||
dialog.set_response_appearance(create_from_template_response, ResponseAppearance::Suggested);
|
||||
dialog.set_response_appearance(cancel_response, ResponseAppearance::Destructive);
|
||||
|
||||
let response = dialog.choose_future(main_window).await;
|
||||
|
||||
// Return if the user chose 'cancel_response'
|
||||
if response == cancel_response {
|
||||
println!("Cancel");
|
||||
return;
|
||||
};
|
||||
|
||||
if response == create_from_template_response {
|
||||
popups::select_pipewire_device::select_pipewire_device(main_window).await;
|
||||
return;
|
||||
};
|
||||
|
||||
if response == create_new_response {
|
||||
popups::new_device_config::new_device_config(
|
||||
main_window,
|
||||
None,
|
||||
HashMap::new(),
|
||||
Vec::new(),
|
||||
None
|
||||
).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,17 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use super::*;
|
||||
use crate::window::popups;
|
||||
|
||||
use adw::ResponseAppearance;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceManagerWindow) {
|
||||
let device_config_names = main_window.imp().state_manager.borrow_mut().get_device_config_names();
|
||||
|
||||
if device_config_names.is_empty() {
|
||||
popups::new_device_config::new_device_config(main_window).await;
|
||||
popups::new_device_config_type::new_device_config_type(main_window).await;
|
||||
return
|
||||
}
|
||||
|
||||
@ -35,10 +35,10 @@ pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceMan
|
||||
.css_classes(["boxed-list"])
|
||||
.build();
|
||||
|
||||
let selected_config_name = Rc::new(RefCell::new(String::new()));
|
||||
let selected_config_id = Rc::new(RefCell::new(None));
|
||||
|
||||
let mut device_configs_list_len = 0;
|
||||
for device_config_row in build_device_config_rows(main_window, &selected_config_name) {
|
||||
for device_config_row in build_device_config_rows(main_window, &selected_config_id) {
|
||||
device_configs_list_len += 1;
|
||||
device_configs_list.append(&device_config_row);
|
||||
}
|
||||
@ -49,15 +49,18 @@ pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceMan
|
||||
.child(&dialog_body)
|
||||
.build();
|
||||
|
||||
let device_configs_list_scrollable = gtk::ScrolledWindow::builder()
|
||||
.child(&device_configs_list)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
|
||||
println!("{}", device_configs_list.height());
|
||||
|
||||
dialog_body.append(&entry);
|
||||
dialog_body.append(&device_configs_list_scrollable);
|
||||
if device_configs_list_len > 3 {
|
||||
let device_configs_list_scrollable = gtk::ScrolledWindow::builder()
|
||||
.child(&device_configs_list)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
dialog_body.append(&device_configs_list_scrollable);
|
||||
} else {
|
||||
dialog_body.append(&device_configs_list);
|
||||
}
|
||||
|
||||
// Create new dialog
|
||||
let dialog = adw::AlertDialog::builder()
|
||||
@ -111,26 +114,27 @@ pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceMan
|
||||
}
|
||||
|
||||
if response == create_new_response {
|
||||
popups::new_device_config::new_device_config(main_window).await;
|
||||
popups::new_device_config_type::new_device_config_type(main_window).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let selected_row = device_configs_list.selected_row().unwrap();
|
||||
|
||||
let config_name = selected_config_name.borrow().clone();
|
||||
let config_id = selected_config_id.borrow().clone().unwrap();
|
||||
|
||||
if response == duplicate_response {
|
||||
let mut state_manager = main_window.imp().state_manager.borrow_mut();
|
||||
|
||||
let properties = state_manager.get_device_config_properties(&config_name).clone();
|
||||
let profiles = state_manager.get_device_config_profiles(&config_name).clone();
|
||||
let selected_profile = state_manager.get_device_config_selected_profile(&config_name).clone();
|
||||
let config_name = state_manager.get_device_config_name(&config_id);
|
||||
let properties = state_manager.get_device_config_properties(&config_id).clone();
|
||||
let profiles = state_manager.get_device_config_profiles(&config_id).clone();
|
||||
let selected_profile = state_manager.get_device_config_selected_profile(&config_id).clone();
|
||||
|
||||
drop(state_manager);
|
||||
|
||||
popups::new_device_config_name::new_device_config_name(
|
||||
popups::new_device_config::new_device_config(
|
||||
main_window,
|
||||
Some(selected_config_name.borrow().clone()),
|
||||
Some(config_name),
|
||||
properties,
|
||||
profiles,
|
||||
selected_profile
|
||||
@ -139,30 +143,34 @@ pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceMan
|
||||
}
|
||||
|
||||
if response == select_response {
|
||||
main_window.add_device_config(&config_name);
|
||||
main_window.add_device_config(config_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn build_device_config_rows(
|
||||
main_window: &AudioDeviceManagerWindow,
|
||||
selected_config_name: &Rc<RefCell<String>>,
|
||||
selected_config_id: &Rc<RefCell<Option<Uuid>>>,
|
||||
) -> Vec<adw::ActionRow> {
|
||||
let mut row_vec = Vec::new();
|
||||
|
||||
let device_names = main_window.imp().state_manager.borrow_mut().get_device_config_names();
|
||||
let mut device_names: Vec<_> = main_window.imp().state_manager.borrow_mut()
|
||||
.get_device_config_names()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
device_names.sort_by(|(_, v1), (_, v2)| v1.cmp(v2));
|
||||
|
||||
for config_name in device_names {
|
||||
for (config_id, config_name) in device_names {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(config_name.clone())
|
||||
.build();
|
||||
|
||||
row.connect_activate(clone!(
|
||||
#[weak]
|
||||
selected_config_name,
|
||||
selected_config_id,
|
||||
move |row| {
|
||||
*selected_config_name.borrow_mut() = config_name.clone();
|
||||
*selected_config_id.borrow_mut() = Some(config_id.clone());
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@ -5,7 +5,6 @@ use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use adw::ResponseAppearance;
|
||||
use crate::window::popups;
|
||||
use crate::window::subpages::device_config;
|
||||
pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceManagerWindow) {
|
||||
|
||||
let cancel_response = "cancel";
|
||||
@ -40,15 +39,20 @@ pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceM
|
||||
.child(&dialog_body)
|
||||
.build();
|
||||
|
||||
let device_list_scrollable = gtk::ScrolledWindow::builder()
|
||||
.child(&device_list)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
|
||||
println!("{}", device_list.height());
|
||||
|
||||
dialog_body.append(&entry);
|
||||
dialog_body.append(&device_list_scrollable);
|
||||
|
||||
if device_list_len > 3 {
|
||||
let device_list_scrollable = gtk::ScrolledWindow::builder()
|
||||
.child(&device_list)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
dialog_body.append(&device_list_scrollable);
|
||||
} else {
|
||||
dialog_body.append(&device_list);
|
||||
}
|
||||
|
||||
// Create new dialog
|
||||
let dialog = adw::AlertDialog::builder()
|
||||
@ -72,6 +76,8 @@ pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceM
|
||||
dialog.set_response_appearance(select_response, ResponseAppearance::Suggested);
|
||||
dialog.set_response_appearance(cancel_response, ResponseAppearance::Destructive);
|
||||
|
||||
let device_name = main_window.imp().device_navigation_page.title();
|
||||
|
||||
device_list.connect_row_selected(clone!(
|
||||
#[weak]
|
||||
dialog,
|
||||
@ -116,11 +122,11 @@ pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceM
|
||||
let mut identifying_properties = HashMap::new();
|
||||
|
||||
identifying_properties.insert(
|
||||
"device.name".to_string(),
|
||||
"device.name".to_owned(),
|
||||
device_properties.get("device.name").unwrap().clone()
|
||||
);
|
||||
|
||||
popups::new_device_config_name::new_device_config_name(
|
||||
popups::new_device_config::new_device_config(
|
||||
main_window,
|
||||
Some(device_properties.get("device.description").unwrap().clone()),
|
||||
identifying_properties,
|
||||
@ -137,9 +143,12 @@ fn build_pipewire_device_rows(
|
||||
) -> Vec<adw::ActionRow> {
|
||||
let mut row_vec = Vec::new();
|
||||
|
||||
let device_names = main_window.imp().pipewire_manager.get_device_names();
|
||||
let mut device_names: Vec<_> = main_window.imp().pipewire_manager
|
||||
.get_device_names()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
println!("{:?}", device_names);
|
||||
device_names.sort_by(|(_, v1), (_, v2)| v1.cmp(v2));
|
||||
|
||||
for (device_id, device_name) in device_names {
|
||||
let row = adw::ActionRow::builder()
|
||||
|
||||
@ -1,54 +1,67 @@
|
||||
use adw::gdk::pango::EllipsizeMode;
|
||||
use crate::window::AudioDeviceManagerWindow;
|
||||
use crate::window::{popups, AudioDeviceManagerWindow};
|
||||
use adw::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use adw::glib::clone;
|
||||
use gtk::{glib, gio};
|
||||
use crate::state_manager::structs::DeviceConfig;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(in crate::window) fn switch_to_device_config_window(main_window: &AudioDeviceManagerWindow, device_config_name: String) {
|
||||
let device_config_window = build_device_config_window(main_window, device_config_name);
|
||||
pub(in crate::window) fn switch_to_device_config_window(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) {
|
||||
let device_config_window = build_device_config_window(main_window, device_config_id);
|
||||
|
||||
main_window.imp().split_view.set_content(Some(&device_config_window));
|
||||
}
|
||||
|
||||
fn build_device_config_window(main_window: &AudioDeviceManagerWindow, device_config_name: String) -> adw::NavigationPage {
|
||||
fn build_device_config_window(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) -> adw::NavigationPage {
|
||||
|
||||
let (navigation_page, clamp) = build_page_base(main_window, device_config_name);
|
||||
let (navigation_page, preferences_page) = page_base::build_page_base(main_window, device_config_id);
|
||||
|
||||
preferences_page.add(
|
||||
&profile_selection::build_profile_selection(main_window, device_config_id),
|
||||
);
|
||||
|
||||
navigation_page
|
||||
}
|
||||
|
||||
/*
|
||||
mod page_base {
|
||||
use super::*;
|
||||
pub(super) fn build_page_base(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) -> (adw::NavigationPage, adw::PreferencesPage) {
|
||||
let preferences_page = adw::PreferencesPage::builder()
|
||||
.build();
|
||||
|
||||
[top]
|
||||
Adw.HeaderBar {
|
||||
show-title: true;
|
||||
[end]
|
||||
MenuButton {
|
||||
primary: true;
|
||||
icon-name: "open-menu-symbolic";
|
||||
tooltip-text: _("Main Menu");
|
||||
menu-model: primary_menu;
|
||||
}
|
||||
}
|
||||
let clamp = adw::Clamp::builder()
|
||||
.child(&preferences_page)
|
||||
.maximum_size(640)
|
||||
.build();
|
||||
|
||||
content: Gtk.ScrolledWindow {
|
||||
child: Adw.Clamp device_page_clamp {
|
||||
maximum-size: 640;
|
||||
let scrolled_window = gtk::ScrolledWindow::builder()
|
||||
.child(&clamp)
|
||||
.build();
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
*/
|
||||
let toolbar_view = adw::ToolbarView::builder()
|
||||
.content(&scrolled_window)
|
||||
.build();
|
||||
|
||||
fn build_page_base(main_window: &AudioDeviceManagerWindow, device_name: String) -> (adw::NavigationPage, adw::Clamp) {
|
||||
let back_button= gtk::Button::builder()
|
||||
.icon_name("go-previous")
|
||||
.build();
|
||||
|
||||
back_button.connect_clicked(clone!(
|
||||
let header_bar = build_header_bar(main_window, device_config_id);
|
||||
|
||||
toolbar_view.add_top_bar(&header_bar);
|
||||
|
||||
let navigation_page = adw::NavigationPage::builder()
|
||||
.child(&toolbar_view)
|
||||
.build();
|
||||
|
||||
(navigation_page, preferences_page)
|
||||
}
|
||||
|
||||
fn build_header_bar(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) -> adw::HeaderBar {
|
||||
let back_button= gtk::Button::builder()
|
||||
.icon_name("go-previous")
|
||||
.build();
|
||||
|
||||
back_button.connect_clicked(clone!(
|
||||
#[weak]
|
||||
main_window,
|
||||
move |button| {
|
||||
@ -60,40 +73,89 @@ fn build_page_base(main_window: &AudioDeviceManagerWindow, device_name: String)
|
||||
}
|
||||
));
|
||||
|
||||
let clamp = adw::Clamp::builder()
|
||||
.maximum_size(640)
|
||||
.build();
|
||||
let header_bar = adw::HeaderBar::builder()
|
||||
.show_title(true)
|
||||
.build();
|
||||
|
||||
let scrolled_window = gtk::ScrolledWindow::builder()
|
||||
.child(&clamp)
|
||||
.build();
|
||||
header_bar.pack_start(&back_button);
|
||||
|
||||
let header_bar = gtk::HeaderBar::builder()
|
||||
.build();
|
||||
let device_config_name = main_window.imp().state_manager.borrow().get_device_config_name(&device_config_id);
|
||||
|
||||
header_bar.pack_start(&back_button);
|
||||
let title_label = gtk::Label::builder()
|
||||
.label(&device_config_name)
|
||||
.single_line_mode(true)
|
||||
.ellipsize(EllipsizeMode::End)
|
||||
.width_chars(5)
|
||||
.css_classes(["title"])
|
||||
.build();
|
||||
|
||||
let title_label = gtk::Label::builder()
|
||||
.label(&device_name)
|
||||
.single_line_mode(true)
|
||||
.ellipsize(EllipsizeMode::End)
|
||||
.width_chars(5)
|
||||
.css_classes(["title"])
|
||||
.build();
|
||||
let title_edit_button = gtk::Button::builder()
|
||||
.icon_name("edit")
|
||||
.build();
|
||||
|
||||
header_bar.set_title_widget(Some(&title_label));
|
||||
title_edit_button.connect_clicked(clone!(
|
||||
#[weak]
|
||||
title_label,
|
||||
#[weak]
|
||||
main_window,
|
||||
move |_| {
|
||||
glib::spawn_future_local(clone!(
|
||||
#[weak]
|
||||
main_window,
|
||||
#[weak]
|
||||
title_label,
|
||||
async move {
|
||||
let Some(new_device_name) = popups::change_device_config_name::change_device_config_name(
|
||||
&main_window,
|
||||
device_config_id
|
||||
).await else { return };
|
||||
|
||||
let toolbar_view = adw::ToolbarView::builder()
|
||||
.content(&scrolled_window)
|
||||
.build();
|
||||
title_label.set_label(&new_device_name)
|
||||
}
|
||||
));
|
||||
}
|
||||
));
|
||||
|
||||
toolbar_view.add_top_bar(&header_bar);
|
||||
let title = gtk::Box::builder()
|
||||
.build();
|
||||
|
||||
let navigation_page = adw::NavigationPage::builder()
|
||||
.child(&toolbar_view)
|
||||
.build();
|
||||
title.append(&title_label);
|
||||
title.append(&title_edit_button);
|
||||
|
||||
println!("device name: {}", device_name);
|
||||
header_bar.set_title_widget(Some(&title));
|
||||
|
||||
header_bar
|
||||
}
|
||||
|
||||
(navigation_page, clamp)
|
||||
}
|
||||
|
||||
mod profile_selection {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn build_profile_selection(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) -> adw::PreferencesGroup {
|
||||
|
||||
let preferences_group = adw::PreferencesGroup::builder()
|
||||
.build();
|
||||
|
||||
let combo_row = adw::ComboRow::builder()
|
||||
.title("Profile")
|
||||
.build();
|
||||
|
||||
let profile_dropdown = gtk::DropDown::builder()
|
||||
.build();
|
||||
|
||||
let properties = main_window.imp().state_manager.borrow().get_device_config_properties(&device_config_id).clone();
|
||||
|
||||
let device_ids = main_window.imp().pipewire_manager.get_device_ids_from_properties(properties);
|
||||
|
||||
if device_ids.len() == 1 {
|
||||
let id = device_ids[0];
|
||||
|
||||
main_window.imp().pipewire_manager.register_profile_combo_row(id, combo_row.clone());
|
||||
}
|
||||
|
||||
preferences_group.add(&combo_row);
|
||||
|
||||
preferences_group
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user