2025-11-07-23-29

This commit is contained in:
Ada Baumann (she/her) 2025-11-07 23:29:58 +01:00
parent 7df8124933
commit 97cb8ff476
18 changed files with 1542 additions and 604 deletions

16
Behavior.txt Normal file
View 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.

View File

@ -5,13 +5,15 @@ edition = "2021"
[dependencies] [dependencies]
dirs = "6.0.0" dirs = "6.0.0"
flume = "0.11.1" flume = { version = "0.11.1", features = ["async"] }
gettext-rs = { version = "0.7", features = ["gettext-system"] } 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" lazy_static = "1.5.0"
libspa = "0.8.0" libspa = "0.8.0"
libspa-sys = "0.8.0"
pipewire = "0.8.0" pipewire = "0.8.0"
sled = "0.34.7" sled = "0.34.7"
uuid = { version = "1.18.0", features = ["v4", "v7"] }
[dependencies.adw] [dependencies.adw]
package = "libadwaita" package = "libadwaita"

View File

@ -95,3 +95,96 @@ __________________________
| Cancel | | 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?

View File

@ -18,14 +18,16 @@
* *
* SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
extern crate core;
mod application; mod application;
mod config; mod config;
mod window; mod window;
mod components; mod components;
mod pw_manager; mod pipewire_manager;
mod utils; mod utils;
mod state_manager; mod state_manager;
mod spa_structs;
use self::application::AudioDeviceManagerApplication; use self::application::AudioDeviceManagerApplication;
use self::window::AudioDeviceManagerWindow; use self::window::AudioDeviceManagerWindow;

676
src/pipewire_manager.rs Normal file
View 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, &registry);
/*
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,
})
}
}

View File

@ -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(&registry);
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, &registry);
/*
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
View 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)
},
}
}
}

View File

@ -1,8 +1,10 @@
use adw::prelude::*;
use std::cell::{Ref, RefCell, RefMut}; use std::cell::{Ref, RefCell, RefMut};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use adw::gdk::Device; use adw::gdk::Device;
use uuid::Uuid;
use crate::state_manager::simple_serde::SimpleSerde; use crate::state_manager::simple_serde::SimpleSerde;
use crate::state_manager::structs::DeviceConfig; use crate::state_manager::structs::DeviceConfig;
@ -80,10 +82,13 @@ pub(crate) mod backends {
pub(crate) mod structs { 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; use crate::state_manager::StateBackend;
pub(crate) struct DeviceConfig { pub(crate) struct DeviceConfig {
id: Uuid,
name: String, name: String,
identifying_properties: HashMap<String, String>, identifying_properties: HashMap<String, String>,
available_profiles: Vec<String>, available_profiles: Vec<String>,
@ -99,7 +104,10 @@ pub(crate) mod structs {
backend: &mut Backend, backend: &mut Backend,
namespace: &Option<String>, namespace: &Option<String>,
) -> Self { ) -> Self {
let id = Uuid::now_v7();
let instance = Self { let instance = Self {
id,
name, name,
identifying_properties, identifying_properties,
available_profiles, available_profiles,
@ -110,15 +118,26 @@ pub(crate) mod structs {
instance 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 { let device_namespace_string = match namespace {
Some(ns) => ns.clone() + "/" + &name, Some(ns) => ns.clone() + "/" + &id.to_string(),
None => name.clone() None => id.to_string()
}; };
let properties_namespace = Some(device_namespace_string.clone() + "/" + "identifying_properties"); let properties_namespace = Some(device_namespace_string.clone() + "/" + "identifying_properties");
let device_namespace = Some(device_namespace_string); 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 properties = backend.get("properties", &device_namespace).unwrap_or(Vec::new());
let identifying_properties = properties.into_iter() let identifying_properties = properties.into_iter()
@ -129,32 +148,28 @@ pub(crate) mod structs {
.collect(); .collect();
Self { 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"), available_profiles: backend.get("available_profiles", &device_namespace).expect("available_profiles not found"),
selected_profile: backend.get("selected_profile", &device_namespace), selected_profile: backend.get("selected_profile", &device_namespace),
identifying_properties identifying_properties
} }
} }
pub(super) fn save<Backend: StateBackend>(&self, backend: &mut Backend, namespace: &Option<String>) -> Result<(), String> { 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()); .unwrap_or(Vec::new());
if devices.contains(&self.name) { if devices.contains(&self.id) {
return Err("Two devices with the same name cannot exist".to_string()) Self::remove_by_id(self.id, backend, namespace);
} else {
devices.push(self.id);
} }
devices.push(self.name.clone());
backend.insert("devices", &devices, &namespace); backend.insert("devices", &devices, &namespace);
let device_namespace_string = match namespace { let (device_namespace, properties_namespace) = Self::get_namespaces(self.id, 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);
backend.insert("name", &self.name, &device_namespace);
backend.insert("available_profiles", &self.available_profiles, &device_namespace); backend.insert("available_profiles", &self.available_profiles, &device_namespace);
backend.insert_option("selected_profile", &self.selected_profile, &device_namespace); backend.insert_option("selected_profile", &self.selected_profile, &device_namespace);
@ -171,10 +186,50 @@ pub(crate) mod structs {
Ok(()) 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 { pub(crate) fn get_name(&self) -> &str {
&self.name &self.name
} }
pub(crate) fn get_id(&self) -> Uuid { self.id }
pub(crate) fn get_identifying_properties(&self) -> &HashMap<String, String> { pub(crate) fn get_identifying_properties(&self) -> &HashMap<String, String> {
&self.identifying_properties &self.identifying_properties
} }
@ -186,11 +241,64 @@ pub(crate) mod structs {
pub(crate) fn get_selected_profile(&self) -> &Option<String> { pub(crate) fn get_selected_profile(&self) -> &Option<String> {
&self.selected_profile &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 { pub(crate) mod simple_serde {
use std::fmt::Debug; use std::fmt::Debug;
use uuid::Uuid;
pub(crate) trait SimpleSerde { pub(crate) trait SimpleSerde {
fn serialize(&self) -> Vec<u8>; fn serialize(&self) -> Vec<u8>;
@ -266,10 +374,22 @@ pub(crate) mod simple_serde {
res 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 { enum RootNamespaces {
None, None,
Devices,
DeviceConfigs, DeviceConfigs,
} }
@ -277,59 +397,78 @@ impl From<RootNamespaces> for Option<String> {
fn from(value: RootNamespaces) -> Self { fn from(value: RootNamespaces) -> Self {
match value { match value {
RootNamespaces::None => None, 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> { pub(crate) struct StateManager<Backend: StateBackend> {
db: Backend, 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> { impl<Backend: StateBackend> StateManager<Backend> {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
let mut instance = Self { let mut instance = Self {
db: Backend::new(crate::utils::CONFIG_DIR.clone().join("db")), db: Backend::new(crate::utils::CONFIG_DIR.clone().join("db")),
selected_device: String::new(),
device_configs: HashMap::new(), device_configs: HashMap::new(),
device_config_action_rows: HashMap::new()
}; };
instance.load(); instance.load();
instance instance
} }
pub(crate) fn load(&mut self) { 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()); .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 { for id in device_config_ids {
let device_config = DeviceConfig::load(device_config_name.clone(), &mut self.db, &RootNamespaces::DeviceConfigs.into()); let device_config = DeviceConfig::load(id, &mut self.db, &RootNamespaces::DeviceConfigs.into());
self.device_configs.insert(device_config_name, device_config); 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() self.device_configs.iter()
.map(|(name, _)| name.clone()) .map(|(id, _)| *id)
.collect() .collect()
} }
pub(crate) fn get_device_config_properties(&mut self, name: &str) -> &HashMap<String, String> { pub(crate) fn get_device_config_name(&self, device_config_id: &Uuid) -> String {
self.device_configs.get(name) 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() .unwrap()
.get_identifying_properties() .get_identifying_properties()
} }
pub(crate) fn get_device_config_profiles(&mut self, name: &str) -> &Vec<String> { pub(crate) fn get_device_config_profiles(&self, device_config_id: &Uuid) -> &Vec<String> {
self.device_configs.get(name) self.device_configs.get(device_config_id)
.unwrap() .unwrap()
.get_available_profiles() .get_available_profiles()
} }
pub(crate) fn get_device_config_selected_profile(&mut self, name: &str) -> &Option<String> { pub(crate) fn get_device_config_selected_profile(&self, device_config_id: &Uuid) -> &Option<String> {
self.device_configs.get(name) self.device_configs.get(device_config_id)
.unwrap() .unwrap()
.get_selected_profile() .get_selected_profile()
} }
@ -340,7 +479,7 @@ impl<Backend: StateBackend> StateManager<Backend> {
identifying_properties: HashMap<String, String>, identifying_properties: HashMap<String, String>,
profiles: Vec<String>, profiles: Vec<String>,
active_profile: Option<String> active_profile: Option<String>
) { ) -> Uuid {
let config = DeviceConfig::new( let config = DeviceConfig::new(
name.clone(), name.clone(),
identifying_properties, identifying_properties,
@ -350,7 +489,76 @@ impl<Backend: StateBackend> StateManager<Backend> {
&RootNamespaces::DeviceConfigs.into() &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
} }
} }

View File

@ -0,0 +1,5 @@
use crate::window::AudioDeviceManagerWindow;
async fn message_handler(main_window: &AudioDeviceManagerWindow) {
}

View File

@ -20,6 +20,7 @@
*/ */
mod popups; mod popups;
mod subpages; mod subpages;
mod message_handler;
use std::cell::{Ref, RefCell, RefMut}; use std::cell::{Ref, RefCell, RefMut};
use std::cmp::min; use std::cmp::min;
@ -31,6 +32,7 @@ use gtk::prelude::*;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use gtk::{gio, glib}; use gtk::{gio, glib};
use uuid::Uuid;
use subpages::device_config; use subpages::device_config;
#[derive(Default)] #[derive(Default)]
@ -60,7 +62,7 @@ impl Default for RcProceduralChildManager {
} }
mod imp { mod imp {
use crate::pw_manager::PipewireManager; use crate::pipewire_manager::PipewireManager;
use crate::state_manager::RcStateManager; use crate::state_manager::RcStateManager;
use crate::state_manager::backends::SledBackend; use crate::state_manager::backends::SledBackend;
use super::*; use super::*;
@ -202,7 +204,7 @@ impl AudioDeviceManagerWindow {
.child() .child()
.and_downcast() .and_downcast()
.expect("No Label in Row"); .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.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() let row = adw::ActionRow::builder()
.title(config_name)
.activatable(true) .activatable(true)
.build(); .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( row.connect_activated(
clone!( clone!(
#[weak(rename_to = window)] #[weak(rename_to = window)]
self, self,
move |row| { move |row| {
println!("Row selected {:?}", row); let device_name = String::from(row.title());
device_config::switch_to_device_config_window(&window, config_id);
} }
) )
); );

View 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!()
}

View File

@ -7,6 +7,8 @@ use crate::window::AudioDeviceManagerWindow;
pub(super) mod new_device; pub(super) mod new_device;
pub(super) mod select_device_config; 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 select_pipewire_device;
mod new_device_config_name; mod new_device_config_name;

View File

@ -1,51 +1,25 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::window::popups::new_device_config_name::new_device_config_name;
use crate::window::subpages;
use super::*; use super::*;
use adw::ResponseAppearance;
use crate::window::popups;
pub(in crate::window) async fn new_device_config(main_window: &AudioDeviceManagerWindow) { pub(in crate::window) async fn new_device_config(
let cancel_response = "cancel"; main_window: &AudioDeviceManagerWindow,
let create_from_template_response = "create-from-template"; name_suggestion: Option<String>,
let create_new_response = "create-new"; properties: HashMap<String, String>,
profiles: Vec<String>,
selected_profile: Option<String>,
// Create new dialog ) {
let dialog = adw::AlertDialog::builder() let Some(name) = new_device_config_name(main_window, name_suggestion).await else { return; };
.heading("New Configuration")
.close_response(cancel_response)
.default_response(create_from_template_response)
.build();
dialog.add_responses(&[ let config_id = main_window.imp().state_manager.borrow_mut().create_new_device_config(
(cancel_response, "Cancel"), name.clone(),
(create_new_response, "Create From Scratch"), properties,
(create_from_template_response, "Use Pipewire Device as Template"), profiles,
]); selected_profile
);
// Make the dialog button insensitive initially main_window.add_device_config(config_id);
dialog.set_response_appearance(create_from_template_response, ResponseAppearance::Suggested); subpages::device_config::switch_to_device_config_window(main_window, config_id);
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;
}
} }

View File

@ -8,10 +8,7 @@ use super::*;
pub(in crate::window) async fn new_device_config_name( pub(in crate::window) async fn new_device_config_name(
main_window: &AudioDeviceManagerWindow, main_window: &AudioDeviceManagerWindow,
name_suggestion: Option<String>, name_suggestion: Option<String>,
properties: HashMap<String, String>, ) -> Option<String> {
profiles: Vec<String>,
selected_profile: Option<String>,
) {
let entry = gtk::Entry::builder() let entry = gtk::Entry::builder()
.placeholder_text("Name") .placeholder_text("Name")
.activates_default(true) .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_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested); 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 { if let Some(init_name) = name_suggestion {
entry.set_text(&init_name); entry.set_text(&init_name);
@ -72,22 +72,14 @@ pub(in crate::window) async fn new_device_config_name(
if response == cancel_response { if response == cancel_response {
println!("Cancel"); println!("Cancel");
return; return None;
}; };
if response == create_response { if response == create_response {
let device_config_name = entry.text().to_string(); let device_config_name = entry.text().to_string();
main_window.imp().state_manager.borrow_mut().create_new_device_config( return Some(device_config_name);
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;
}; };
unreachable!()
} }

View 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;
}
}

View File

@ -1,17 +1,17 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::cmp::min; use std::cmp::min;
use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use super::*; use super::*;
use crate::window::popups; use crate::window::popups;
use adw::ResponseAppearance; use adw::ResponseAppearance;
use uuid::Uuid;
pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceManagerWindow) { 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(); let device_config_names = main_window.imp().state_manager.borrow_mut().get_device_config_names();
if device_config_names.is_empty() { 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 return
} }
@ -35,10 +35,10 @@ pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceMan
.css_classes(["boxed-list"]) .css_classes(["boxed-list"])
.build(); .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; 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_len += 1;
device_configs_list.append(&device_config_row); 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) .child(&dialog_body)
.build(); .build();
println!("{}", device_configs_list.height());
dialog_body.append(&entry);
if device_configs_list_len > 3 {
let device_configs_list_scrollable = gtk::ScrolledWindow::builder() let device_configs_list_scrollable = gtk::ScrolledWindow::builder()
.child(&device_configs_list) .child(&device_configs_list)
.vexpand(true) .vexpand(true)
.build(); .build();
println!("{}", device_configs_list.height());
dialog_body.append(&entry);
dialog_body.append(&device_configs_list_scrollable); dialog_body.append(&device_configs_list_scrollable);
} else {
dialog_body.append(&device_configs_list);
}
// Create new dialog // Create new dialog
let dialog = adw::AlertDialog::builder() 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 { 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; return;
} }
let selected_row = device_configs_list.selected_row().unwrap(); 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 { if response == duplicate_response {
let mut state_manager = main_window.imp().state_manager.borrow_mut(); let mut state_manager = main_window.imp().state_manager.borrow_mut();
let properties = state_manager.get_device_config_properties(&config_name).clone(); let config_name = state_manager.get_device_config_name(&config_id);
let profiles = state_manager.get_device_config_profiles(&config_name).clone(); let properties = state_manager.get_device_config_properties(&config_id).clone();
let selected_profile = state_manager.get_device_config_selected_profile(&config_name).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); drop(state_manager);
popups::new_device_config_name::new_device_config_name( popups::new_device_config::new_device_config(
main_window, main_window,
Some(selected_config_name.borrow().clone()), Some(config_name),
properties, properties,
profiles, profiles,
selected_profile selected_profile
@ -139,30 +143,34 @@ pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceMan
} }
if response == select_response { if response == select_response {
main_window.add_device_config(&config_name); main_window.add_device_config(config_id);
return; return;
} }
} }
fn build_device_config_rows( fn build_device_config_rows(
main_window: &AudioDeviceManagerWindow, main_window: &AudioDeviceManagerWindow,
selected_config_name: &Rc<RefCell<String>>, selected_config_id: &Rc<RefCell<Option<Uuid>>>,
) -> Vec<adw::ActionRow> { ) -> Vec<adw::ActionRow> {
let mut row_vec = Vec::new(); 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() let row = adw::ActionRow::builder()
.title(config_name.clone()) .title(config_name.clone())
.build(); .build();
row.connect_activate(clone!( row.connect_activate(clone!(
#[weak] #[weak]
selected_config_name, selected_config_id,
move |row| { move |row| {
*selected_config_name.borrow_mut() = config_name.clone(); *selected_config_id.borrow_mut() = Some(config_id.clone());
} }
) )
); );

View File

@ -5,7 +5,6 @@ use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use adw::ResponseAppearance; use adw::ResponseAppearance;
use crate::window::popups; use crate::window::popups;
use crate::window::subpages::device_config;
pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceManagerWindow) { pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceManagerWindow) {
let cancel_response = "cancel"; let cancel_response = "cancel";
@ -40,15 +39,20 @@ pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceM
.child(&dialog_body) .child(&dialog_body)
.build(); .build();
let device_list_scrollable = gtk::ScrolledWindow::builder()
.child(&device_list)
.vexpand(true)
.build();
println!("{}", device_list.height()); println!("{}", device_list.height());
dialog_body.append(&entry); dialog_body.append(&entry);
if device_list_len > 3 {
let device_list_scrollable = gtk::ScrolledWindow::builder()
.child(&device_list)
.vexpand(true)
.build();
dialog_body.append(&device_list_scrollable); dialog_body.append(&device_list_scrollable);
} else {
dialog_body.append(&device_list);
}
// Create new dialog // Create new dialog
let dialog = adw::AlertDialog::builder() 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(select_response, ResponseAppearance::Suggested);
dialog.set_response_appearance(cancel_response, ResponseAppearance::Destructive); dialog.set_response_appearance(cancel_response, ResponseAppearance::Destructive);
let device_name = main_window.imp().device_navigation_page.title();
device_list.connect_row_selected(clone!( device_list.connect_row_selected(clone!(
#[weak] #[weak]
dialog, dialog,
@ -116,11 +122,11 @@ pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceM
let mut identifying_properties = HashMap::new(); let mut identifying_properties = HashMap::new();
identifying_properties.insert( identifying_properties.insert(
"device.name".to_string(), "device.name".to_owned(),
device_properties.get("device.name").unwrap().clone() 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, main_window,
Some(device_properties.get("device.description").unwrap().clone()), Some(device_properties.get("device.description").unwrap().clone()),
identifying_properties, identifying_properties,
@ -137,9 +143,12 @@ fn build_pipewire_device_rows(
) -> Vec<adw::ActionRow> { ) -> Vec<adw::ActionRow> {
let mut row_vec = Vec::new(); 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 { for (device_id, device_name) in device_names {
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()

View File

@ -1,49 +1,62 @@
use adw::gdk::pango::EllipsizeMode; use adw::gdk::pango::EllipsizeMode;
use crate::window::AudioDeviceManagerWindow; use crate::window::{popups, AudioDeviceManagerWindow};
use adw::prelude::*; use adw::prelude::*;
use gtk::prelude::*; use gtk::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use adw::glib::clone; use adw::glib::clone;
use gtk::{glib, gio}; 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) { 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_name); let device_config_window = build_device_config_window(main_window, device_config_id);
main_window.imp().split_view.set_content(Some(&device_config_window)); 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 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] let clamp = adw::Clamp::builder()
Adw.HeaderBar { .child(&preferences_page)
show-title: true; .maximum_size(640)
[end] .build();
MenuButton {
primary: true; let scrolled_window = gtk::ScrolledWindow::builder()
icon-name: "open-menu-symbolic"; .child(&clamp)
tooltip-text: _("Main Menu"); .build();
menu-model: primary_menu;
}
let toolbar_view = adw::ToolbarView::builder()
.content(&scrolled_window)
.build();
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)
} }
content: Gtk.ScrolledWindow { fn build_header_bar(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) -> adw::HeaderBar {
child: Adw.Clamp device_page_clamp {
maximum-size: 640;
};
};
*/
fn build_page_base(main_window: &AudioDeviceManagerWindow, device_name: String) -> (adw::NavigationPage, adw::Clamp) {
let back_button= gtk::Button::builder() let back_button= gtk::Button::builder()
.icon_name("go-previous") .icon_name("go-previous")
.build(); .build();
@ -60,40 +73,89 @@ fn build_page_base(main_window: &AudioDeviceManagerWindow, device_name: String)
} }
)); ));
let clamp = adw::Clamp::builder() let header_bar = adw::HeaderBar::builder()
.maximum_size(640) .show_title(true)
.build();
let scrolled_window = gtk::ScrolledWindow::builder()
.child(&clamp)
.build();
let header_bar = gtk::HeaderBar::builder()
.build(); .build();
header_bar.pack_start(&back_button); header_bar.pack_start(&back_button);
let device_config_name = main_window.imp().state_manager.borrow().get_device_config_name(&device_config_id);
let title_label = gtk::Label::builder() let title_label = gtk::Label::builder()
.label(&device_name) .label(&device_config_name)
.single_line_mode(true) .single_line_mode(true)
.ellipsize(EllipsizeMode::End) .ellipsize(EllipsizeMode::End)
.width_chars(5) .width_chars(5)
.css_classes(["title"]) .css_classes(["title"])
.build(); .build();
header_bar.set_title_widget(Some(&title_label)); let title_edit_button = gtk::Button::builder()
.icon_name("edit")
let toolbar_view = adw::ToolbarView::builder()
.content(&scrolled_window)
.build(); .build();
toolbar_view.add_top_bar(&header_bar); 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 navigation_page = adw::NavigationPage::builder() title_label.set_label(&new_device_name)
.child(&toolbar_view) }
));
}
));
let title = gtk::Box::builder()
.build(); .build();
println!("device name: {}", device_name); title.append(&title_label);
title.append(&title_edit_button);
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
}
} }