i3status_rs/blocks/sound/
alsa.rs1use 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 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}