1use std::borrow::Borrow;
2use std::cell::RefCell;
3use std::rc::Rc;
4
5use fauxpas::{*, Context as _};
6use libpulse_binding::callbacks::ListResult;
7use libpulse_binding::context::{self, Context};
8use libpulse_binding::mainloop::standard::{Mainloop, IterateResult};
9use libpulse_binding::operation;
10use libpulse_binding::proplist::Proplist;
11use libpulse_binding::proplist::properties::APPLICATION_NAME;
12use libpulse_binding::volume::{ChannelVolumes, Volume};
13use structopt::StructOpt;
14
15#[derive(StructOpt)]
16pub enum Opt {
17 Get,
18 Set { percent: u8 },
19 #[structopt(visible_alias = "up")]
20 Increase(VolumeChange),
21 #[structopt(visible_alias = "down")]
22 Decrease(VolumeChange),
23 ToggleMute,
24}
25
26#[derive(StructOpt)]
27pub struct VolumeChange {
28 #[structopt(default_value = "1")]
29 amount: u8,
31}
32
33pub fn run(opt: &Opt) -> Result<()> {
34 match opt {
35 Opt::Get => cmd_get(),
36 Opt::Set { percent } => cmd_set(*percent),
37 Opt::Increase(opt) => cmd_increase(opt),
38 Opt::Decrease(opt) => cmd_decrease(opt),
39 Opt::ToggleMute => cmd_toggle_mute(),
40 }
41}
42
43fn cmd_get() -> Result<()> {
44 let (mut mainloop, context) = connect_to_pulseaudio()?;
45
46 let sink_info = get_sink_info_by_name(&mut mainloop, &context, "@DEFAULT_SINK@")
47 .context("Failed to get sink info")?;
48
49 let volume = sink_info.volume.max();
50
51 println!("{:.0}", volume.percent());
52
53 Ok(())
54}
55
56fn cmd_set(percent: u8) -> Result<()> {
57 let (mut mainloop, context) = connect_to_pulseaudio()?;
58 let percent = (percent as f32).clamp(0., 100.);
59
60 let sink_info = get_sink_info_by_name(&mut mainloop, &context, "@DEFAULT_SINK@")
61 .context("Failed to get sink info")?;
62
63 let mut volume = sink_info.volume;
64
65 for volume in volume.get_mut() {
66 volume.set_percent(percent);
67 }
68
69 set_sink_volume_by_name(&mut mainloop, &context, "@DEFAULT_SINK@", &volume)
70 .context("Failed to set sink volume")?;
71
72 Ok(())
73}
74
75fn cmd_increase(opt: &VolumeChange) -> Result<()> {
76 let (mut mainloop, context) = connect_to_pulseaudio()?;
77 let amount = (opt.amount as f32).clamp(0., 100.);
78
79 let sink_info = get_sink_info_by_name(&mut mainloop, &context, "@DEFAULT_SINK@")
80 .context("Failed to get sink info")?;
81
82 let mut volume = sink_info.volume;
83
84 for volume in volume.get_mut() {
85 let new_percent = (volume.percent() + amount)
86 .round()
87 .clamp(0., 100.);
88
89 volume.set_percent(new_percent);
90 }
91
92 set_sink_volume_by_name(&mut mainloop, &context, "@DEFAULT_SINK@", &volume)
93 .context("Failed to set sink volume")?;
94
95 Ok(())
96}
97
98fn cmd_decrease(opt: &VolumeChange) -> Result<()> {
99 let (mut mainloop, context) = connect_to_pulseaudio()?;
100 let amount = (opt.amount as f32).clamp(0., 100.);
101
102 let sink_info = get_sink_info_by_name(&mut mainloop, &context, "@DEFAULT_SINK@")
103 .context("Failed to get sink info")?;
104
105 let mut volume = sink_info.volume;
106
107 for volume in volume.get_mut() {
108 let new_percent = (volume.percent() - amount)
109 .round()
110 .clamp(0., 100.);
111
112 volume.set_percent(new_percent);
113 }
114
115 set_sink_volume_by_name(&mut mainloop, &context, "@DEFAULT_SINK@", &volume)
116 .context("Failed to set sink volume")?;
117
118 Ok(())
119}
120
121fn cmd_toggle_mute() -> Result<()> {
122 let (mut mainloop, context) = connect_to_pulseaudio()?;
123 let sink_name = "@DEFAULT_SINK@";
124
125 let sink_info = get_sink_info_by_name(&mut mainloop, &context, sink_name)
126 .context("Failed to get sink info")?;
127
128 let mute = !sink_info.mute;
129
130 set_sink_mute_by_name(&mut mainloop, &context, sink_name, mute)
131 .context("Failed to set sink mute flag")?;
132
133 Ok(())
134}
135
136fn connect_to_pulseaudio() -> Result<(Mainloop, Context)> {
137 let mut mainloop = Mainloop::new()
138 .context("Failed to create main loop")?;
139
140 let mut proplist = Proplist::new()
141 .context("Failed to create proplist")?;
142 proplist.set_str(APPLICATION_NAME, "frob")
143 .map_err(|()| anyhow!("Failed to set application name"))?;
144
145 let mut context = Context::new_with_proplist(&*mainloop.borrow(), "FrobContext", &proplist)
146 .context("Failed to create context")?;
147
148 context.connect(None, context::FlagSet::NOFLAGS, None)
149 .context("Failed to connect to pulseaudio")?;
150
151 loop {
152 match mainloop.iterate(true) {
153 IterateResult::Quit(_) |
154 IterateResult::Err(_) => {
155 bail!("Iterate state was not success, quitting...");
156 },
157 IterateResult::Success(_) => {},
158 }
159
160 match context.borrow().get_state() {
161 context::State::Ready => { break; },
162 context::State::Failed |
163 context::State::Terminated => {
164 bail!("Context state failed/terminated, quitting...");
165 },
166 _ => {},
167 }
168 }
169
170 Ok((mainloop, context))
171}
172
173fn get_sink_info_by_name(mainloop: &mut Mainloop, context: &Context, name: &str) -> Result<SinkInfo> {
174 let introspector = context.introspect();
175 let sink_info_result = Rc::new(RefCell::new(None::<Result<SinkInfo, ()>>));
176
177 let operation = introspector.get_sink_info_by_name(name, {
178 let sink_info_result = sink_info_result.clone();
179
180 move |list| {
181 let mut sink_info_result = sink_info_result.borrow_mut();
182
183 match list {
184 ListResult::Item(sink_info) => *sink_info_result = Some(Result::Ok(SinkInfo {
185 volume: sink_info.volume,
186 mute: sink_info.mute,
187 })),
188 ListResult::End => {},
189 ListResult::Error => *sink_info_result = Some(Err(())),
190 }
191 }
192 });
193
194 loop {
195 match mainloop.iterate(true) {
196 IterateResult::Quit(_) |
197 IterateResult::Err(_) => {
198 bail!("Iterate state was not success, quitting...");
199 },
200 IterateResult::Success(_) => {},
201 }
202
203 match operation.get_state() {
204 operation::State::Running => {},
205 operation::State::Done => break,
206 operation::State::Cancelled => break,
207 }
208 }
209
210 let sink_info = Rc::try_unwrap(sink_info_result)
211 .map_err(|_| anyhow!("Failed to regain ownership of sink info result"))?
212 .into_inner()
213 .with_context(|| anyhow!("Sink not found: {}", name))?
214 .map_err(|_: ()| fauxpas!("Faled to get sink info"))?;
215
216 Ok(sink_info)
217}
218
219fn set_sink_volume_by_name(
220 mainloop: &mut Mainloop,
221 context: &Context,
222 name: &str,
223 volume: &ChannelVolumes,
224) -> Result<()> {
225 let mut introspector = context.introspect();
226 let operation = introspector.set_sink_volume_by_name(name, volume, None);
227
228 loop {
229 match mainloop.iterate(true) {
230 IterateResult::Quit(_) |
231 IterateResult::Err(_) => {
232 bail!("Iterate state was not success, quitting...");
233 },
234 IterateResult::Success(_) => {},
235 }
236
237 match operation.get_state() {
238 operation::State::Running => {},
239 operation::State::Done => break,
240 operation::State::Cancelled => break,
241 }
242 }
243
244 Ok(())
245}
246
247fn set_sink_mute_by_name(
248 mainloop: &mut Mainloop,
249 context: &Context,
250 name: &str,
251 mute: bool,
252) -> Result<()> {
253 let mut introspector = context.introspect();
254 let operation = introspector.set_sink_mute_by_name(name, mute, None);
255
256 loop {
257 match mainloop.iterate(true) {
258 IterateResult::Quit(_) |
259 IterateResult::Err(_) => {
260 bail!("Iterate state was not success, quitting...");
261 },
262 IterateResult::Success(_) => {},
263 }
264
265 match operation.get_state() {
266 operation::State::Running => {},
267 operation::State::Done => break,
268 operation::State::Cancelled => break,
269 }
270 }
271
272 Ok(())
273}
274
275trait VolumeExt {
276 fn percent(&self) -> f32;
277 fn set_percent(&mut self, percent: f32);
278}
279
280impl VolumeExt for Volume {
281 fn percent(&self) -> f32 {
282 self.0 as f32 / Volume::NORMAL.0 as f32 * 100.
283 }
284
285 fn set_percent(&mut self, percent: f32) {
286 self.0 = (percent / 100. * Volume::NORMAL.0 as f32) as u32;
287 }
288}
289
290#[derive(Debug)]
291struct SinkInfo {
292 volume: ChannelVolumes,
293 mute: bool,
294}