i3status_rs/blocks/sound/
alsa.rs

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