Skip to main content

qemu_command_builder/args/
audio.rs

1use std::str::FromStr;
2
3use crate::parsers::{ARG_AUDIO, DELIM_COMMA};
4use crate::to_command::ToCommand;
5use bon::Builder;
6use proptest_derive::Arbitrary;
7
8const KEY_DRIVER: &str = "driver=";
9const KEY_MODEL: &str = "model=";
10
11/// A generic `-audio` property rendered after `driver=` and `model=`.
12#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
13pub struct AudioProperty {
14    key: String,
15    value: Option<String>,
16}
17
18impl AudioProperty {
19    /// Creates a `key=value` property.
20    pub fn with_value(key: impl Into<String>, value: impl Into<String>) -> Self {
21        Self {
22            key: key.into(),
23            value: Some(value.into()),
24        }
25    }
26
27    /// Creates a bare `key` property with no explicit value.
28    pub fn flag(key: impl Into<String>) -> Self {
29        Self { key: key.into(), value: None }
30    }
31}
32
33/// QEMU `-audio [driver=]driver[,model=value][,prop[=value][,...]]`.
34///
35/// This shortcut configures a default host audio backend and optionally the
36/// guest audio device model. Additional backend properties are emitted after
37/// the canonical `driver` and `model` fields.
38#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Default, Builder, Arbitrary)]
39pub struct Audio {
40    driver: String,
41    model: Option<String>,
42    #[builder(default)]
43    properties: Vec<AudioProperty>,
44}
45
46impl Audio {
47    /// Creates an audio configuration for the given backend driver.
48    pub fn new(driver: impl Into<String>) -> Self {
49        Self {
50            driver: driver.into(),
51            model: None,
52            properties: Vec::new(),
53        }
54    }
55
56    /// Sets the guest audio device model used by the `-audio` shortcut.
57    pub fn model(&mut self, model: impl Into<String>) -> &mut Self {
58        self.model = Some(model.into());
59        self
60    }
61
62    /// Adds a `key=value` backend property.
63    pub fn add_prop<K: AsRef<str>, V: AsRef<str>>(&mut self, key: K, value: V) -> &mut Self {
64        self.properties.push(AudioProperty::with_value(key.as_ref(), value.as_ref()));
65        self
66    }
67
68    /// Adds a bare backend property with no explicit value.
69    pub fn add_flag<K: AsRef<str>>(&mut self, key: K) -> &mut Self {
70        self.properties.push(AudioProperty::flag(key.as_ref()));
71        self
72    }
73}
74
75impl ToCommand for Audio {
76    fn command(&self) -> String {
77        ARG_AUDIO.to_string()
78    }
79
80    fn to_args(&self) -> Vec<String> {
81        let mut args = vec![format!("{}{}", KEY_DRIVER, self.driver)];
82
83        if let Some(model) = &self.model {
84            args.push(format!("{}{}", KEY_MODEL, model));
85        }
86        for property in &self.properties {
87            match &property.value {
88                Some(value) => args.push(format!("{}={}", property.key, value)),
89                None => args.push(property.key.clone()),
90            }
91        }
92
93        vec![args.join(DELIM_COMMA)]
94    }
95}
96
97impl FromStr for Audio {
98    type Err = String;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        let mut parts = s.split(DELIM_COMMA);
102        let first = parts.next().ok_or_else(|| "empty audio argument".to_string())?;
103        let driver = first.strip_prefix(KEY_DRIVER).unwrap_or(first).to_string();
104        if driver.is_empty() {
105            return Err("missing audio driver".to_string());
106        }
107
108        let mut audio = Audio::new(driver);
109
110        for part in parts {
111            match part.split_once('=') {
112                Some(("model", value)) => audio.model = Some(value.to_string()),
113                Some((key, value)) => audio.properties.push(AudioProperty::with_value(key, value)),
114                None => audio.properties.push(AudioProperty::flag(part)),
115            }
116        }
117
118        Ok(audio)
119    }
120}