gardesk/gartray / 968443b

Browse files

Refactor volume to use pactl, remove libpulse dependency

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
968443bea97038031d70f1b2e58bd27f19770a18
Parents
925e1bb
Tree
965f974

3 changed files

StatusFile+-
M Cargo.toml 0 3
M gartray/Cargo.toml 0 3
M gartray/src/panel/volume.rs 132 149
Cargo.tomlmodified
@@ -43,9 +43,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
4343
 anyhow = "1.0"
4444
 thiserror = "1.0"
4545
 
46
-# Audio (PulseAudio)
47
-libpulse-binding = "2.0"
48
-
4946
 # Utilities
5047
 dirs = "5.0"
5148
 shellexpand = "3.0"
gartray/Cargo.tomlmodified
@@ -43,9 +43,6 @@ tracing-subscriber = { workspace = true }
4343
 anyhow = { workspace = true }
4444
 thiserror = { workspace = true }
4545
 
46
-# Audio
47
-libpulse-binding = { workspace = true }
48
-
4946
 # Utilities
5047
 dirs = { workspace = true }
5148
 shellexpand = { workspace = true }
gartray/src/panel/volume.rsmodified
@@ -1,15 +1,10 @@
1
-//! Volume control module using PulseAudio
1
+//! Volume control module using pactl
22
 //!
33
 //! Provides volume slider and mute toggle for audio output.
4
+//! Uses pactl commands - works with both PulseAudio and PipeWire.
45
 
56
 use anyhow::{Context, Result};
6
-use libpulse_binding as pulse;
7
-use libpulse_binding::context::Context as PulseContext;
8
-use libpulse_binding::mainloop::standard::Mainloop;
9
-use libpulse_binding::context::subscribe::InterestMaskSet;
10
-use std::cell::RefCell;
11
-use std::rc::Rc;
12
-use std::sync::{Arc, Mutex};
7
+use std::process::Command;
138
 use tracing::{debug, info, warn};
149
 
1510
 /// Volume state
@@ -21,62 +16,37 @@ pub struct VolumeState {
2116
     pub muted: bool,
2217
     /// Sink name
2318
     pub sink_name: String,
24
-    /// Sink description
25
-    pub sink_description: String,
2619
 }
2720
 
28
-/// Volume module for controlling PulseAudio
21
+/// Volume module for controlling audio via pactl
2922
 pub struct VolumeModule {
30
-    mainloop: Option<Rc<RefCell<Mainloop>>>,
31
-    context: Option<Rc<RefCell<PulseContext>>>,
32
-    state: Arc<Mutex<VolumeState>>,
33
-    connected: bool,
23
+    state: VolumeState,
24
+    available: bool,
3425
 }
3526
 
3627
 impl VolumeModule {
3728
     /// Create a new volume module
3829
     pub fn new() -> Self {
3930
         Self {
40
-            mainloop: None,
41
-            context: None,
42
-            state: Arc::new(Mutex::new(VolumeState::default())),
43
-            connected: false,
31
+            state: VolumeState::default(),
32
+            available: false,
4433
         }
4534
     }
4635
 
47
-    /// Connect to PulseAudio
36
+    /// Connect and check if pactl is available
4837
     pub fn connect(&mut self) -> Result<()> {
49
-        let mainloop = Rc::new(RefCell::new(
50
-            Mainloop::new().context("Failed to create PulseAudio mainloop")?
51
-        ));
52
-
53
-        let context = Rc::new(RefCell::new(
54
-            PulseContext::new(&*mainloop.borrow(), "gartray")
55
-                .context("Failed to create PulseAudio context")?
56
-        ));
57
-
58
-        // Connect
59
-        context.borrow_mut()
60
-            .connect(None, pulse::context::FlagSet::NOFLAGS, None)
61
-            .map_err(|_| anyhow::anyhow!("Failed to connect to PulseAudio"))?;
62
-
63
-        // Wait for connection
64
-        loop {
65
-            mainloop.borrow_mut().iterate(true);
66
-            match context.borrow().get_state() {
67
-                pulse::context::State::Ready => break,
68
-                pulse::context::State::Failed |
69
-                pulse::context::State::Terminated => {
70
-                    return Err(anyhow::anyhow!("PulseAudio connection failed"));
71
-                }
72
-                _ => {}
73
-            }
38
+        // Check if pactl is available
39
+        let output = Command::new("pactl")
40
+            .arg("--version")
41
+            .output()
42
+            .context("pactl not found")?;
43
+
44
+        if !output.status.success() {
45
+            anyhow::bail!("pactl not working");
7446
         }
7547
 
48
+        self.available = true;
7649
         info!("Connected to PulseAudio");
77
-        self.mainloop = Some(mainloop);
78
-        self.context = Some(context);
79
-        self.connected = true;
8050
 
8151
         // Get initial state
8252
         self.refresh()?;
@@ -84,158 +54,171 @@ impl VolumeModule {
8454
         Ok(())
8555
     }
8656
 
87
-    /// Refresh volume state from PulseAudio
57
+    /// Refresh volume state from pactl
8858
     pub fn refresh(&mut self) -> Result<()> {
89
-        if !self.connected {
59
+        if !self.available {
9060
             return Ok(());
9161
         }
9262
 
93
-        let context = self.context.as_ref()
94
-            .ok_or_else(|| anyhow::anyhow!("Not connected"))?;
95
-        let mainloop = self.mainloop.as_ref()
96
-            .ok_or_else(|| anyhow::anyhow!("No mainloop"))?;
63
+        // Get default sink
64
+        if let Ok(sink) = self.get_default_sink() {
65
+            self.state.sink_name = sink;
66
+        }
67
+
68
+        // Get volume and mute state
69
+        if let Ok((volume, muted)) = self.get_sink_volume(&self.state.sink_name) {
70
+            self.state.volume = volume;
71
+            self.state.muted = muted;
72
+        }
9773
 
98
-        let state = self.state.clone();
74
+        debug!("Volume: {:.0}%, muted: {}", self.state.volume * 100.0, self.state.muted);
75
+        Ok(())
76
+    }
9977
 
100
-        // Get default sink info
101
-        let introspector = context.borrow().introspect();
102
-        let op = introspector.get_server_info(move |info| {
103
-            if let Some(default_sink) = &info.default_sink_name {
104
-                let mut s = state.lock().unwrap();
105
-                s.sink_name = default_sink.to_string();
106
-            }
107
-        });
108
-
109
-        // Wait for operation
110
-        while op.get_state() == pulse::operation::State::Running {
111
-            mainloop.borrow_mut().iterate(true);
112
-        }
113
-
114
-        // Now get sink details
115
-        let state = self.state.clone();
116
-        let sink_name = {
117
-            let s = state.lock().unwrap();
118
-            s.sink_name.clone()
119
-        };
120
-
121
-        if !sink_name.is_empty() {
122
-            let introspector = context.borrow().introspect();
123
-            let op = introspector.get_sink_info_by_name(&sink_name, move |result| {
124
-                if let pulse::callbacks::ListResult::Item(sink) = result {
125
-                    let mut s = state.lock().unwrap();
126
-                    s.muted = sink.mute;
127
-                    if let Some(desc) = &sink.description {
128
-                        s.sink_description = desc.to_string();
78
+    /// Get the default sink name
79
+    fn get_default_sink(&self) -> Result<String> {
80
+        let output = Command::new("pactl")
81
+            .args(["get-default-sink"])
82
+            .output()
83
+            .context("Failed to get default sink")?;
84
+
85
+        if output.status.success() {
86
+            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
87
+        } else {
88
+            anyhow::bail!("pactl get-default-sink failed")
89
+        }
90
+    }
91
+
92
+    /// Get volume and mute state for a sink
93
+    fn get_sink_volume(&self, sink: &str) -> Result<(f64, bool)> {
94
+        // Get volume
95
+        let output = Command::new("pactl")
96
+            .args(["get-sink-volume", sink])
97
+            .output()
98
+            .context("Failed to get sink volume")?;
99
+
100
+        let mut volume = 0.5;
101
+        if output.status.success() {
102
+            let stdout = String::from_utf8_lossy(&output.stdout);
103
+            // Parse output like "Volume: front-left: 65536 / 100% / 0.00 dB, ..."
104
+            if let Some(pct) = stdout.split('/').nth(1) {
105
+                if let Some(num) = pct.trim().strip_suffix('%') {
106
+                    if let Ok(v) = num.trim().parse::<f64>() {
107
+                        volume = v / 100.0;
129108
                     }
130
-                    // Calculate average volume
131
-                    let vol = sink.volume.avg().0 as f64 / pulse::volume::Volume::NORMAL.0 as f64;
132
-                    s.volume = vol.min(1.5); // Cap at 150%
133
-                    debug!("Volume: {:.0}%, muted: {}", s.volume * 100.0, s.muted);
134109
                 }
135
-            });
136
-
137
-            while op.get_state() == pulse::operation::State::Running {
138
-                mainloop.borrow_mut().iterate(true);
139110
             }
140111
         }
141112
 
142
-        Ok(())
113
+        // Get mute state
114
+        let output = Command::new("pactl")
115
+            .args(["get-sink-mute", sink])
116
+            .output()
117
+            .context("Failed to get sink mute")?;
118
+
119
+        let mut muted = false;
120
+        if output.status.success() {
121
+            let stdout = String::from_utf8_lossy(&output.stdout);
122
+            muted = stdout.contains("yes");
123
+        }
124
+
125
+        Ok((volume, muted))
143126
     }
144127
 
145128
     /// Set volume (0.0 - 1.0)
146129
     pub fn set_volume(&mut self, volume: f64) -> Result<()> {
147
-        if !self.connected {
130
+        if !self.available {
148131
             return Ok(());
149132
         }
150133
 
151
-        let context = self.context.as_ref()
152
-            .ok_or_else(|| anyhow::anyhow!("Not connected"))?;
153
-        let mainloop = self.mainloop.as_ref()
154
-            .ok_or_else(|| anyhow::anyhow!("No mainloop"))?;
155
-
156
-        let sink_name = {
157
-            let s = self.state.lock().unwrap();
158
-            s.sink_name.clone()
159
-        };
160
-
161
-        if sink_name.is_empty() {
134
+        let sink = &self.state.sink_name;
135
+        if sink.is_empty() {
162136
             return Ok(());
163137
         }
164138
 
165
-        // Convert to PulseAudio volume
166
-        let pa_vol = (volume.clamp(0.0, 1.5) * pulse::volume::Volume::NORMAL.0 as f64) as u32;
167
-        let mut channel_vol = pulse::volume::ChannelVolumes::default();
168
-        channel_vol.set(2, pulse::volume::Volume(pa_vol));
139
+        // Convert to percentage
140
+        let pct = (volume.clamp(0.0, 1.5) * 100.0) as u32;
141
+        let volume_str = format!("{}%", pct);
169142
 
170
-        let mut introspector = context.borrow().introspect();
171
-        let op = introspector.set_sink_volume_by_name(&sink_name, &channel_vol, None);
172
-
173
-        while op.get_state() == pulse::operation::State::Running {
174
-            mainloop.borrow_mut().iterate(true);
175
-        }
143
+        let status = Command::new("pactl")
144
+            .args(["set-sink-volume", sink, &volume_str])
145
+            .status()
146
+            .context("Failed to set volume")?;
176147
 
177
-        // Update local state
178
-        {
179
-            let mut s = self.state.lock().unwrap();
180
-            s.volume = volume;
148
+        if status.success() {
149
+            self.state.volume = volume;
150
+            debug!("Set volume to {:.0}%", volume * 100.0);
151
+        } else {
152
+            warn!("pactl set-sink-volume failed");
181153
         }
182154
 
183
-        debug!("Set volume to {:.0}%", volume * 100.0);
184155
         Ok(())
185156
     }
186157
 
187158
     /// Toggle mute
188159
     pub fn toggle_mute(&mut self) -> Result<()> {
189
-        if !self.connected {
160
+        if !self.available {
190161
             return Ok(());
191162
         }
192163
 
193
-        let context = self.context.as_ref()
194
-            .ok_or_else(|| anyhow::anyhow!("Not connected"))?;
195
-        let mainloop = self.mainloop.as_ref()
196
-            .ok_or_else(|| anyhow::anyhow!("No mainloop"))?;
164
+        let sink = &self.state.sink_name;
165
+        if sink.is_empty() {
166
+            return Ok(());
167
+        }
197168
 
198
-        let (sink_name, muted) = {
199
-            let s = self.state.lock().unwrap();
200
-            (s.sink_name.clone(), s.muted)
201
-        };
169
+        let status = Command::new("pactl")
170
+            .args(["set-sink-mute", sink, "toggle"])
171
+            .status()
172
+            .context("Failed to toggle mute")?;
202173
 
203
-        if sink_name.is_empty() {
204
-            return Ok(());
174
+        if status.success() {
175
+            self.state.muted = !self.state.muted;
176
+            debug!("Mute toggled to {}", self.state.muted);
177
+        } else {
178
+            warn!("pactl set-sink-mute failed");
205179
         }
206180
 
207
-        let mut introspector = context.borrow().introspect();
208
-        let op = introspector.set_sink_mute_by_name(&sink_name, !muted, None);
181
+        Ok(())
182
+    }
183
+
184
+    /// Set mute state
185
+    pub fn set_mute(&mut self, muted: bool) -> Result<()> {
186
+        if !self.available {
187
+            return Ok(());
188
+        }
209189
 
210
-        while op.get_state() == pulse::operation::State::Running {
211
-            mainloop.borrow_mut().iterate(true);
190
+        let sink = &self.state.sink_name;
191
+        if sink.is_empty() {
192
+            return Ok(());
212193
         }
213194
 
214
-        // Update local state
215
-        {
216
-            let mut s = self.state.lock().unwrap();
217
-            s.muted = !muted;
195
+        let mute_str = if muted { "1" } else { "0" };
196
+
197
+        let status = Command::new("pactl")
198
+            .args(["set-sink-mute", sink, mute_str])
199
+            .status()
200
+            .context("Failed to set mute")?;
201
+
202
+        if status.success() {
203
+            self.state.muted = muted;
218204
         }
219205
 
220
-        debug!("Mute toggled to {}", !muted);
221206
         Ok(())
222207
     }
223208
 
224209
     /// Get current state
225210
     pub fn state(&self) -> VolumeState {
226
-        self.state.lock().unwrap().clone()
211
+        self.state.clone()
227212
     }
228213
 
229
-    /// Check if connected
230
-    pub fn is_connected(&self) -> bool {
231
-        self.connected
214
+    /// Check if available
215
+    pub fn is_available(&self) -> bool {
216
+        self.available
232217
     }
233218
 }
234219
 
235
-impl Drop for VolumeModule {
236
-    fn drop(&mut self) {
237
-        if let Some(context) = &self.context {
238
-            context.borrow_mut().disconnect();
239
-        }
220
+impl Default for VolumeModule {
221
+    fn default() -> Self {
222
+        Self::new()
240223
     }
241224
 }