From 97cb8ff4768950f771ea6e83f2dc129592af55fa Mon Sep 17 00:00:00 2001 From: Ada Baumann Date: Fri, 7 Nov 2025 23:29:58 +0100 Subject: [PATCH] 2025-11-07-23-29 --- Behavior.txt | 16 + Cargo.toml | 6 +- UI Drafts.txt | 93 +++ src/main.rs | 4 +- src/pipewire_manager.rs | 676 ++++++++++++++++++ src/pw_manager.rs | 410 ----------- src/spa_structs.rs | 151 ++++ src/state_manager.rs | 276 ++++++- src/window/message_handler.rs | 5 + src/window/mod.rs | 22 +- .../popups/change_device_config_name.rs | 89 +++ src/window/popups/mod.rs | 4 +- src/window/popups/new_device_config.rs | 64 +- src/window/popups/new_device_config_name.rs | 24 +- src/window/popups/new_device_config_type.rs | 51 ++ src/window/popups/select_device_config.rs | 54 +- src/window/popups/select_pipewire_device.rs | 29 +- src/window/subpages/device_config.rs | 172 +++-- 18 files changed, 1542 insertions(+), 604 deletions(-) create mode 100644 Behavior.txt create mode 100644 src/pipewire_manager.rs delete mode 100644 src/pw_manager.rs create mode 100644 src/spa_structs.rs create mode 100644 src/window/message_handler.rs create mode 100644 src/window/popups/change_device_config_name.rs create mode 100644 src/window/popups/new_device_config_type.rs diff --git a/Behavior.txt b/Behavior.txt new file mode 100644 index 0000000..9ce11e2 --- /dev/null +++ b/Behavior.txt @@ -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. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index c7e8e81..1e67950 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/UI Drafts.txt b/UI Drafts.txt index 6601e6f..03e6c54 100644 --- a/UI Drafts.txt +++ b/UI Drafts.txt @@ -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? \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3d8a38a..2a54ee7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; diff --git a/src/pipewire_manager.rs b/src/pipewire_manager.rs new file mode 100644 index 0000000..4456962 --- /dev/null +++ b/src/pipewire_manager.rs @@ -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 +} + +impl RegisteredObjects { + fn new() -> Self { + Self { + profile_combo_rows: HashMap::new() + } + } +} + +struct Device { + properties: HashMap, + profiles: HashMap, + active_profile: Option, +} + +impl Default for Device { + fn default() -> Self { + Self { + properties: HashMap::new(), + profiles: HashMap::new(), + active_profile: None, + } + } +} + +pub(crate) struct PipewireManager { + handle: JoinHandle, + devices: Rc>>, + registered_objects: Rc>, + tx: pipewire::channel::Sender, + rx: flume::Receiver +} + +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, + devices: Rc>>, + registered_objects: Rc> +) { + 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>, 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>, 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 { + 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 { + self.devices.borrow()[&id] + .properties + .clone() + + } + + pub(crate) fn get_device_ids_from_properties(&self, properties: HashMap) -> Vec { + 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; + +// 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>, + device_listener: pipewire::device::DeviceListener, + profiles: Rc>>, +} + +impl DeviceMessageHandler { + fn new(pipewire_device: pipewire::device::Device, id: usize, tx: flume::Sender) -> 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, + profiles: Rc>> + ) -> 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, + pipewire_device: Rc> + ) -> 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, + profiles: Rc>>, + 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, + pipewire_device: Rc> + ) { + // 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>, + devices: HashMap, + tx: flume::Sender, +} + +impl GlobalsManager { + fn new(tx: flume::Sender) -> Self { + Self { + globals: Vec::new(), + devices: HashMap::new(), + tx + } + } + + fn add_global(&mut self, global: GlobalObject, 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, pipewire::channel::Sender, flume::Receiver) { + + 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 { + param + .and_then(|pod| { + PodDeserializer::deserialize_any_from(pod.as_bytes()).ok() + }) + .and_then(|(_, value)| match value { + Value::Object(obj) => Some(obj), + _ => None, + }) + } +} \ No newline at end of file diff --git a/src/pw_manager.rs b/src/pw_manager.rs deleted file mode 100644 index a70b0ea..0000000 --- a/src/pw_manager.rs +++ /dev/null @@ -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), - DeviceProperties(usize, HashMap), -} - -pub(crate) struct PipewireManager { - handle: JoinHandle, - tx: pipewire::channel::Sender, - rx: flume::Receiver, -} - -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 { - 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 { - 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>>, - profiles: Rc>>, - name: Rc>, - id: usize, - properties: Rc>>, - 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>>, - profiles: &Rc>>, - ) -> 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>, - properties: &Rc>>, - ) -> 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>>, - profiles: Rc>>, - ) { - 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>, - properties: Rc>>, - ) { - 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>, - devices: HashMap, -} - -impl GlobalsManager { - fn new() -> Self { - Self { - globals: Vec::new(), - devices: HashMap::new(), - } - } - - fn add_global(&mut self, global: GlobalObject, 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 { - self.devices.iter() - .map(|(id, d)| (*id, d.name.borrow().clone())) - .collect() - } - - fn get_device_properties(&self, id: usize) -> HashMap { - self.devices.get(&id) - .and_then(|d| Some(d.properties.borrow().clone())) - .unwrap() - } -} - -pub(crate) fn spawn_pipewire_thread() -> (JoinHandle, pipewire::channel::Sender, flume::Receiver) { - - 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 { - param - .and_then(|pod| { - PodDeserializer::deserialize_any_from(pod.as_bytes()).ok() - }) - .and_then(|(_, value)| match value { - Value::Object(obj) => Some(obj), - _ => None, - }) - } -} \ No newline at end of file diff --git a/src/spa_structs.rs b/src/spa_structs.rs new file mode 100644 index 0000000..1ff0ad3 --- /dev/null +++ b/src/spa_structs.rs @@ -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 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 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) + }, + } + } +} \ No newline at end of file diff --git a/src/state_manager.rs b/src/state_manager.rs index 9cbf7e7..82ef2c2 100644 --- a/src/state_manager.rs +++ b/src/state_manager.rs @@ -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, available_profiles: Vec, @@ -99,7 +104,10 @@ pub(crate) mod structs { backend: &mut Backend, namespace: &Option, ) -> 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(name: String, backend: &mut Backend, namespace: &Option) -> Self { + + fn get_namespaces(id: Uuid, namespace: &Option) -> (Option, Option) { 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(id: Uuid, backend: &mut Backend, namespace: &Option) -> 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(&self, backend: &mut Backend, namespace: &Option) -> Result<(), String> { - let mut devices: Vec = backend.get("devices", &namespace) + let mut devices: Vec = 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(id: Uuid, backend: &mut Backend, namespace: &Option) { + let mut devices: Vec = 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 = 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(&self, backend: &mut Backend, namespace: &Option) { + Self::remove_by_id(self.id, backend, namespace); + } + + pub(super) fn change_name(&mut self, new_name: String, backend: &mut Backend, namespace: &Option) { + + 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 { &self.identifying_properties } @@ -186,11 +241,64 @@ pub(crate) mod structs { pub(crate) fn get_selected_profile(&self) -> &Option { &self.selected_profile } + + pub(crate) fn get_available_channels(&self) -> HashSet { + 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 + + } + pub(crate) struct Output { + name: String, + output_profile: Uuid, + } + + pub(crate) struct Device { + id: Uuid, + name: String, + device_configs: Vec, + inputs: Vec, + outputs: Vec, + } + + impl Device { + } } pub(crate) mod simple_serde { use std::fmt::Debug; + use uuid::Uuid; pub(crate) trait SimpleSerde { fn serialize(&self) -> Vec; @@ -266,10 +374,22 @@ pub(crate) mod simple_serde { res } } + + impl SimpleSerde for Uuid { + fn serialize(&self) -> Vec { + self.as_bytes().to_vec() + } + + fn deserialize(bytes: Vec) -> 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 for Option { 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 { db: Backend, - device_configs: HashMap + selected_device: String, + device_configs: HashMap, + device_config_action_rows: HashMap>> } impl StateManager { 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 = self.db.get("devices", &RootNamespaces::DeviceConfigs.into()) + let device_config_ids: Vec = 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 { + pub(crate) fn get_device_config_ids(&self) -> Vec { self.device_configs.iter() - .map(|(name, _)| name.clone()) + .map(|(id, _)| *id) .collect() } - pub(crate) fn get_device_config_properties(&mut self, name: &str) -> &HashMap { - 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 { + 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 { + self.device_configs.get(device_config_id) .unwrap() .get_identifying_properties() } - pub(crate) fn get_device_config_profiles(&mut self, name: &str) -> &Vec { - self.device_configs.get(name) + pub(crate) fn get_device_config_profiles(&self, device_config_id: &Uuid) -> &Vec { + self.device_configs.get(device_config_id) .unwrap() .get_available_profiles() } - pub(crate) fn get_device_config_selected_profile(&mut self, name: &str) -> &Option { - self.device_configs.get(name) + pub(crate) fn get_device_config_selected_profile(&self, device_config_id: &Uuid) -> &Option { + self.device_configs.get(device_config_id) .unwrap() .get_selected_profile() } @@ -340,7 +479,7 @@ impl StateManager { identifying_properties: HashMap, profiles: Vec, active_profile: Option - ) { + ) -> Uuid { let config = DeviceConfig::new( name.clone(), identifying_properties, @@ -350,7 +489,76 @@ impl StateManager { &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 } } diff --git a/src/window/message_handler.rs b/src/window/message_handler.rs new file mode 100644 index 0000000..03fec61 --- /dev/null +++ b/src/window/message_handler.rs @@ -0,0 +1,5 @@ +use crate::window::AudioDeviceManagerWindow; + +async fn message_handler(main_window: &AudioDeviceManagerWindow) { + +} \ No newline at end of file diff --git a/src/window/mod.rs b/src/window/mod.rs index ca573cd..af7673e 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -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); } ) ); diff --git a/src/window/popups/change_device_config_name.rs b/src/window/popups/change_device_config_name.rs new file mode 100644 index 0000000..c2dcb81 --- /dev/null +++ b/src/window/popups/change_device_config_name.rs @@ -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 { + 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 = 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!() +} \ No newline at end of file diff --git a/src/window/popups/mod.rs b/src/window/popups/mod.rs index 9b28c01..0ad3405 100644 --- a/src/window/popups/mod.rs +++ b/src/window/popups/mod.rs @@ -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; \ No newline at end of file diff --git a/src/window/popups/new_device_config.rs b/src/window/popups/new_device_config.rs index 820942d..cfcbd4f 100644 --- a/src/window/popups/new_device_config.rs +++ b/src/window/popups/new_device_config.rs @@ -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, + properties: HashMap, + profiles: Vec, + selected_profile: Option, - // 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); +} \ No newline at end of file diff --git a/src/window/popups/new_device_config_name.rs b/src/window/popups/new_device_config_name.rs index 2cc784c..1beff50 100644 --- a/src/window/popups/new_device_config_name.rs +++ b/src/window/popups/new_device_config_name.rs @@ -8,10 +8,7 @@ use super::*; pub(in crate::window) async fn new_device_config_name( main_window: &AudioDeviceManagerWindow, name_suggestion: Option, - properties: HashMap, - profiles: Vec, - selected_profile: Option, -) { +) -> Option { 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 = 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!() } \ No newline at end of file diff --git a/src/window/popups/new_device_config_type.rs b/src/window/popups/new_device_config_type.rs new file mode 100644 index 0000000..8627133 --- /dev/null +++ b/src/window/popups/new_device_config_type.rs @@ -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; + } +} diff --git a/src/window/popups/select_device_config.rs b/src/window/popups/select_device_config.rs index 0938f73..c6c9c0d 100644 --- a/src/window/popups/select_device_config.rs +++ b/src/window/popups/select_device_config.rs @@ -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>, + selected_config_id: &Rc>>, ) -> Vec { 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()); } ) ); diff --git a/src/window/popups/select_pipewire_device.rs b/src/window/popups/select_pipewire_device.rs index 76c608a..c6f90b1 100644 --- a/src/window/popups/select_pipewire_device.rs +++ b/src/window/popups/select_pipewire_device.rs @@ -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 { 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() diff --git a/src/window/subpages/device_config.rs b/src/window/subpages/device_config.rs index 2ec1aaa..f8419f6 100644 --- a/src/window/subpages/device_config.rs +++ b/src/window/subpages/device_config.rs @@ -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 + } +} \ No newline at end of file