#![deny(missing_docs)]
use std::fmt;
pub use volumecontrol_core::AudioError;
pub use volumecontrol_core::DeviceInfo;
use volumecontrol_core::AudioDevice as _;
#[cfg(target_os = "linux")]
use volumecontrol_linux::AudioDevice as Inner;
#[cfg(target_os = "windows")]
use volumecontrol_windows::AudioDevice as Inner;
#[cfg(target_os = "macos")]
use volumecontrol_macos::AudioDevice as Inner;
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
compile_error!(
"volumecontrol does not support the current target OS. \
Supported targets: linux, windows, macos."
);
#[derive(Debug)]
pub struct AudioDevice(Inner);
impl fmt::Display for AudioDevice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl AudioDevice {
pub fn from_default() -> Result<Self, AudioError> {
Inner::from_default().map(Self)
}
pub fn from_id(id: &str) -> Result<Self, AudioError> {
Inner::from_id(id).map(Self)
}
pub fn from_name(name: &str) -> Result<Self, AudioError> {
Inner::from_name(name).map(Self)
}
pub fn list() -> Result<Vec<DeviceInfo>, AudioError> {
Inner::list()
}
pub fn get_vol(&self) -> Result<u8, AudioError> {
self.0.get_vol()
}
pub fn set_vol(&self, vol: u8) -> Result<(), AudioError> {
self.0.set_vol(vol)
}
pub fn is_mute(&self) -> Result<bool, AudioError> {
self.0.is_mute()
}
pub fn set_mute(&self, muted: bool) -> Result<(), AudioError> {
self.0.set_mute(muted)
}
pub fn id(&self) -> &str {
self.0.id()
}
pub fn name(&self) -> &str {
self.0.name()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "windows")]
const BOGUS_ID: &str = "volumecontrol-test-nonexistent-{00000000-0000-0000-0000-000000000000}";
#[cfg(target_os = "macos")]
const BOGUS_ID: &str = "not-a-number";
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
const BOGUS_ID: &str = "__nonexistent_sink_xyz__";
const BOGUS_NAME: &str = "zzz-volumecontrol-test-nonexistent-device-name";
#[test]
fn display_contains_name_and_id() {
let device = AudioDevice::from_default().expect("from_default()");
let s = device.to_string();
assert!(
s.contains(device.name()),
"Display output should contain the device name; got: {s}"
);
assert!(
s.contains(device.id()),
"Display output should contain the device id; got: {s}"
);
let expected = format!("{} ({})", device.name(), device.id());
assert_eq!(s, expected);
}
#[test]
fn default_device_id_and_name_nonempty() {
let device = AudioDevice::from_default().expect("from_default()");
assert!(!device.id().is_empty(), "device id must not be empty");
assert!(!device.name().is_empty(), "device name must not be empty");
}
#[test]
fn default_returns_ok() {
let result = AudioDevice::from_default();
assert!(result.is_ok(), "expected Ok, got {result:?}");
}
#[test]
fn list_returns_nonempty() {
let devices = AudioDevice::list().expect("list()");
assert!(
!devices.is_empty(),
"expected at least one audio device from list()"
);
for info in &devices {
assert!(!info.id.is_empty(), "device id must not be empty");
assert!(!info.name.is_empty(), "device name must not be empty");
}
}
#[test]
fn from_id_valid_id_returns_ok() {
let devices = AudioDevice::list().expect("list()");
let first = devices.first().expect("at least one device in list");
let found = AudioDevice::from_id(&first.id);
assert!(
found.is_ok(),
"from_id with a valid id should succeed, got {found:?}"
);
}
#[test]
fn from_id_nonexistent_returns_err() {
let result = AudioDevice::from_id(BOGUS_ID);
assert!(result.is_err(), "expected an error, got {result:?}");
}
#[test]
fn from_name_partial_match_returns_ok() {
let devices = AudioDevice::list().expect("list()");
let first = devices.first().expect("at least one device in list");
let partial: String = first.name.chars().take(3).collect();
let found = AudioDevice::from_name(&partial);
assert!(
found.is_ok(),
"from_name with partial match '{partial}' should succeed"
);
}
#[test]
fn from_name_no_match_returns_err() {
let result = AudioDevice::from_name(BOGUS_NAME);
assert!(result.is_err(), "expected an error, got {result:?}");
}
#[test]
fn get_vol_returns_valid_range() {
let device = AudioDevice::from_default().expect("from_default()");
let vol = device.get_vol().expect("get_vol()");
assert!(vol <= 100, "volume must be in 0..=100, got {vol}");
}
#[test]
fn set_vol_changes_volume() {
let device = AudioDevice::from_default().expect("from_default()");
let original = device.get_vol().expect("get_vol()");
let target: u8 = if original >= 50 { 30 } else { 70 };
device.set_vol(target).expect("set_vol()");
let after = device.get_vol().expect("get_vol() after set");
assert!(
after.abs_diff(target) <= 1,
"expected volume near {target}, got {after}"
);
device.set_vol(original).expect("restore original volume");
}
#[test]
fn set_mute_changes_mute_state() {
let device = AudioDevice::from_default().expect("from_default()");
let original = device.is_mute().expect("is_mute()");
let target = !original;
device.set_mute(target).expect("set_mute()");
let after = device.is_mute().expect("is_mute() after set");
assert_eq!(after, target, "mute state should be {target}, got {after}");
device
.set_mute(original)
.expect("restore original mute state");
}
}