frob_volume/
lib.rs

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    /// the amount to change the volume by
30    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}