i3status_rs/blocks/sound/
alsa.rs

1use std::cmp::{max, min};
2use std::process::Stdio;
3use tokio::process::Command;
4
5use super::super::prelude::*;
6use super::SoundDevice;
7
8pub(super) struct Device {
9    name: String,
10    device: String,
11    natural_mapping: bool,
12    volume: u32,
13    muted: bool,
14    process: tokio::process::Child,
15}
16
17impl Device {
18    pub(super) fn new(name: String, device: String, natural_mapping: bool) -> Result<Self> {
19        Ok(Device {
20            name,
21            device,
22            natural_mapping,
23            volume: 0,
24            muted: false,
25            process: Command::new("alsactl")
26                .arg("monitor")
27                .kill_on_drop(true)
28                .stdout(Stdio::piped())
29                .spawn()
30                .error("Failed to start alsactl monitor")?,
31        })
32    }
33}
34
35#[async_trait::async_trait]
36impl SoundDevice for Device {
37    fn volume(&self) -> u32 {
38        self.volume
39    }
40
41    fn muted(&self) -> bool {
42        self.muted
43    }
44
45    fn output_name(&self) -> String {
46        self.name.clone()
47    }
48
49    fn output_description(&self) -> Option<String> {
50        // TODO Does Alsa has something similar like descriptions in Pulse?
51        None
52    }
53
54    fn active_port(&self) -> Option<String> {
55        None
56    }
57
58    fn form_factor(&self) -> Option<&str> {
59        None
60    }
61
62    async fn get_info(&mut self) -> Result<()> {
63        let mut args = Vec::new();
64        if self.natural_mapping {
65            args.push("-M");
66        };
67        args.extend(["-D", &self.device, "get", &self.name]);
68
69        let output: String = Command::new("amixer")
70            .args(&args)
71            .output()
72            .await
73            .map(|o| std::str::from_utf8(&o.stdout).unwrap().trim().into())
74            .error("could not run amixer to get sound info")?;
75
76        let last_line = &output.lines().last().error("could not get sound info")?;
77
78        const FILTER: &[char] = &['[', ']', '%'];
79        let mut last = last_line
80            .split_whitespace()
81            .filter(|x| x.starts_with('[') && !x.contains("dB"))
82            .map(|s| s.trim_matches(FILTER));
83
84        self.volume = last
85            .next()
86            .error("could not get volume")?
87            .parse::<u32>()
88            .error("could not parse volume to u32")?;
89
90        self.muted = last.next().map(|muted| muted == "off").unwrap_or(false);
91
92        Ok(())
93    }
94
95    async fn set_volume(&mut self, step: i32, max_vol: Option<u32>) -> Result<()> {
96        let new_vol = max(0, self.volume as i32 + step) as u32;
97        let capped_volume = if let Some(vol_cap) = max_vol {
98            min(new_vol, vol_cap)
99        } else {
100            new_vol
101        };
102        let mut args = Vec::new();
103        if self.natural_mapping {
104            args.push("-M");
105        };
106        let vol_str = format!("{capped_volume}%");
107        args.extend(["-D", &self.device, "set", &self.name, &vol_str]);
108
109        Command::new("amixer")
110            .args(&args)
111            .output()
112            .await
113            .error("failed to set volume")?;
114
115        self.volume = capped_volume;
116
117        Ok(())
118    }
119
120    async fn toggle(&mut self) -> Result<()> {
121        let mut args = Vec::new();
122        if self.natural_mapping {
123            args.push("-M");
124        };
125        args.extend(["-D", &self.device, "set", &self.name, "toggle"]);
126
127        Command::new("amixer")
128            .args(&args)
129            .output()
130            .await
131            .error("failed to toggle mute")?;
132
133        self.muted = !self.muted;
134
135        Ok(())
136    }
137
138    async fn wait_for_update(&mut self) -> Result<()> {
139        let mut buf = [0u8; 1024];
140        self.process
141            .stdout
142            .as_mut()
143            .error("Failed to get stdout of alsactl monitor")?
144            .read(&mut buf)
145            .await
146            .error("Failed to read stdbuf output")?;
147        Ok(())
148    }
149}