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"] }
43
 anyhow = "1.0"
43
 anyhow = "1.0"
44
 thiserror = "1.0"
44
 thiserror = "1.0"
45
 
45
 
46
-# Audio (PulseAudio)
47
-libpulse-binding = "2.0"
48
-
49
 # Utilities
46
 # Utilities
50
 dirs = "5.0"
47
 dirs = "5.0"
51
 shellexpand = "3.0"
48
 shellexpand = "3.0"
gartray/Cargo.tomlmodified
@@ -43,9 +43,6 @@ tracing-subscriber = { workspace = true }
43
 anyhow = { workspace = true }
43
 anyhow = { workspace = true }
44
 thiserror = { workspace = true }
44
 thiserror = { workspace = true }
45
 
45
 
46
-# Audio
47
-libpulse-binding = { workspace = true }
48
-
49
 # Utilities
46
 # Utilities
50
 dirs = { workspace = true }
47
 dirs = { workspace = true }
51
 shellexpand = { workspace = true }
48
 shellexpand = { workspace = true }
gartray/src/panel/volume.rsmodified
@@ -1,15 +1,10 @@
1
-//! Volume control module using PulseAudio
1
+//! Volume control module using pactl
2
 //!
2
 //!
3
 //! Provides volume slider and mute toggle for audio output.
3
 //! Provides volume slider and mute toggle for audio output.
4
+//! Uses pactl commands - works with both PulseAudio and PipeWire.
4
 
5
 
5
 use anyhow::{Context, Result};
6
 use anyhow::{Context, Result};
6
-use libpulse_binding as pulse;
7
+use std::process::Command;
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};
13
 use tracing::{debug, info, warn};
8
 use tracing::{debug, info, warn};
14
 
9
 
15
 /// Volume state
10
 /// Volume state
@@ -21,62 +16,37 @@ pub struct VolumeState {
21
     pub muted: bool,
16
     pub muted: bool,
22
     /// Sink name
17
     /// Sink name
23
     pub sink_name: String,
18
     pub sink_name: String,
24
-    /// Sink description
25
-    pub sink_description: String,
26
 }
19
 }
27
 
20
 
28
-/// Volume module for controlling PulseAudio
21
+/// Volume module for controlling audio via pactl
29
 pub struct VolumeModule {
22
 pub struct VolumeModule {
30
-    mainloop: Option<Rc<RefCell<Mainloop>>>,
23
+    state: VolumeState,
31
-    context: Option<Rc<RefCell<PulseContext>>>,
24
+    available: bool,
32
-    state: Arc<Mutex<VolumeState>>,
33
-    connected: bool,
34
 }
25
 }
35
 
26
 
36
 impl VolumeModule {
27
 impl VolumeModule {
37
     /// Create a new volume module
28
     /// Create a new volume module
38
     pub fn new() -> Self {
29
     pub fn new() -> Self {
39
         Self {
30
         Self {
40
-            mainloop: None,
31
+            state: VolumeState::default(),
41
-            context: None,
32
+            available: false,
42
-            state: Arc::new(Mutex::new(VolumeState::default())),
43
-            connected: false,
44
         }
33
         }
45
     }
34
     }
46
 
35
 
47
-    /// Connect to PulseAudio
36
+    /// Connect and check if pactl is available
48
     pub fn connect(&mut self) -> Result<()> {
37
     pub fn connect(&mut self) -> Result<()> {
49
-        let mainloop = Rc::new(RefCell::new(
38
+        // Check if pactl is available
50
-            Mainloop::new().context("Failed to create PulseAudio mainloop")?
39
+        let output = Command::new("pactl")
51
-        ));
40
+            .arg("--version")
52
-
41
+            .output()
53
-        let context = Rc::new(RefCell::new(
42
+            .context("pactl not found")?;
54
-            PulseContext::new(&*mainloop.borrow(), "gartray")
43
+
55
-                .context("Failed to create PulseAudio context")?
44
+        if !output.status.success() {
56
-        ));
45
+            anyhow::bail!("pactl not working");
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
-            }
74
         }
46
         }
75
 
47
 
48
+        self.available = true;
76
         info!("Connected to PulseAudio");
49
         info!("Connected to PulseAudio");
77
-        self.mainloop = Some(mainloop);
78
-        self.context = Some(context);
79
-        self.connected = true;
80
 
50
 
81
         // Get initial state
51
         // Get initial state
82
         self.refresh()?;
52
         self.refresh()?;
@@ -84,158 +54,171 @@ impl VolumeModule {
84
         Ok(())
54
         Ok(())
85
     }
55
     }
86
 
56
 
87
-    /// Refresh volume state from PulseAudio
57
+    /// Refresh volume state from pactl
88
     pub fn refresh(&mut self) -> Result<()> {
58
     pub fn refresh(&mut self) -> Result<()> {
89
-        if !self.connected {
59
+        if !self.available {
90
             return Ok(());
60
             return Ok(());
91
         }
61
         }
92
 
62
 
93
-        let context = self.context.as_ref()
63
+        // Get default sink
94
-            .ok_or_else(|| anyhow::anyhow!("Not connected"))?;
64
+        if let Ok(sink) = self.get_default_sink() {
95
-        let mainloop = self.mainloop.as_ref()
65
+            self.state.sink_name = sink;
96
-            .ok_or_else(|| anyhow::anyhow!("No mainloop"))?;
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
+        }
97
 
73
 
98
-        let state = self.state.clone();
74
+        debug!("Volume: {:.0}%, muted: {}", self.state.volume * 100.0, self.state.muted);
75
+        Ok(())
76
+    }
99
 
77
 
100
-        // Get default sink info
78
+    /// Get the default sink name
101
-        let introspector = context.borrow().introspect();
79
+    fn get_default_sink(&self) -> Result<String> {
102
-        let op = introspector.get_server_info(move |info| {
80
+        let output = Command::new("pactl")
103
-            if let Some(default_sink) = &info.default_sink_name {
81
+            .args(["get-default-sink"])
104
-                let mut s = state.lock().unwrap();
82
+            .output()
105
-                s.sink_name = default_sink.to_string();
83
+            .context("Failed to get default sink")?;
106
-            }
84
+
107
-        });
85
+        if output.status.success() {
108
-
86
+            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
109
-        // Wait for operation
87
+        } else {
110
-        while op.get_state() == pulse::operation::State::Running {
88
+            anyhow::bail!("pactl get-default-sink failed")
111
-            mainloop.borrow_mut().iterate(true);
89
+        }
112
-        }
90
+    }
113
-
91
+
114
-        // Now get sink details
92
+    /// Get volume and mute state for a sink
115
-        let state = self.state.clone();
93
+    fn get_sink_volume(&self, sink: &str) -> Result<(f64, bool)> {
116
-        let sink_name = {
94
+        // Get volume
117
-            let s = state.lock().unwrap();
95
+        let output = Command::new("pactl")
118
-            s.sink_name.clone()
96
+            .args(["get-sink-volume", sink])
119
-        };
97
+            .output()
120
-
98
+            .context("Failed to get sink volume")?;
121
-        if !sink_name.is_empty() {
99
+
122
-            let introspector = context.borrow().introspect();
100
+        let mut volume = 0.5;
123
-            let op = introspector.get_sink_info_by_name(&sink_name, move |result| {
101
+        if output.status.success() {
124
-                if let pulse::callbacks::ListResult::Item(sink) = result {
102
+            let stdout = String::from_utf8_lossy(&output.stdout);
125
-                    let mut s = state.lock().unwrap();
103
+            // Parse output like "Volume: front-left: 65536 / 100% / 0.00 dB, ..."
126
-                    s.muted = sink.mute;
104
+            if let Some(pct) = stdout.split('/').nth(1) {
127
-                    if let Some(desc) = &sink.description {
105
+                if let Some(num) = pct.trim().strip_suffix('%') {
128
-                        s.sink_description = desc.to_string();
106
+                    if let Ok(v) = num.trim().parse::<f64>() {
107
+                        volume = v / 100.0;
129
                     }
108
                     }
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);
134
                 }
109
                 }
135
-            });
136
-
137
-            while op.get_state() == pulse::operation::State::Running {
138
-                mainloop.borrow_mut().iterate(true);
139
             }
110
             }
140
         }
111
         }
141
 
112
 
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))
143
     }
126
     }
144
 
127
 
145
     /// Set volume (0.0 - 1.0)
128
     /// Set volume (0.0 - 1.0)
146
     pub fn set_volume(&mut self, volume: f64) -> Result<()> {
129
     pub fn set_volume(&mut self, volume: f64) -> Result<()> {
147
-        if !self.connected {
130
+        if !self.available {
148
             return Ok(());
131
             return Ok(());
149
         }
132
         }
150
 
133
 
151
-        let context = self.context.as_ref()
134
+        let sink = &self.state.sink_name;
152
-            .ok_or_else(|| anyhow::anyhow!("Not connected"))?;
135
+        if sink.is_empty() {
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() {
162
             return Ok(());
136
             return Ok(());
163
         }
137
         }
164
 
138
 
165
-        // Convert to PulseAudio volume
139
+        // Convert to percentage
166
-        let pa_vol = (volume.clamp(0.0, 1.5) * pulse::volume::Volume::NORMAL.0 as f64) as u32;
140
+        let pct = (volume.clamp(0.0, 1.5) * 100.0) as u32;
167
-        let mut channel_vol = pulse::volume::ChannelVolumes::default();
141
+        let volume_str = format!("{}%", pct);
168
-        channel_vol.set(2, pulse::volume::Volume(pa_vol));
169
 
142
 
170
-        let mut introspector = context.borrow().introspect();
143
+        let status = Command::new("pactl")
171
-        let op = introspector.set_sink_volume_by_name(&sink_name, &channel_vol, None);
144
+            .args(["set-sink-volume", sink, &volume_str])
172
-
145
+            .status()
173
-        while op.get_state() == pulse::operation::State::Running {
146
+            .context("Failed to set volume")?;
174
-            mainloop.borrow_mut().iterate(true);
175
-        }
176
 
147
 
177
-        // Update local state
148
+        if status.success() {
178
-        {
149
+            self.state.volume = volume;
179
-            let mut s = self.state.lock().unwrap();
150
+            debug!("Set volume to {:.0}%", volume * 100.0);
180
-            s.volume = volume;
151
+        } else {
152
+            warn!("pactl set-sink-volume failed");
181
         }
153
         }
182
 
154
 
183
-        debug!("Set volume to {:.0}%", volume * 100.0);
184
         Ok(())
155
         Ok(())
185
     }
156
     }
186
 
157
 
187
     /// Toggle mute
158
     /// Toggle mute
188
     pub fn toggle_mute(&mut self) -> Result<()> {
159
     pub fn toggle_mute(&mut self) -> Result<()> {
189
-        if !self.connected {
160
+        if !self.available {
190
             return Ok(());
161
             return Ok(());
191
         }
162
         }
192
 
163
 
193
-        let context = self.context.as_ref()
164
+        let sink = &self.state.sink_name;
194
-            .ok_or_else(|| anyhow::anyhow!("Not connected"))?;
165
+        if sink.is_empty() {
195
-        let mainloop = self.mainloop.as_ref()
166
+            return Ok(());
196
-            .ok_or_else(|| anyhow::anyhow!("No mainloop"))?;
167
+        }
197
 
168
 
198
-        let (sink_name, muted) = {
169
+        let status = Command::new("pactl")
199
-            let s = self.state.lock().unwrap();
170
+            .args(["set-sink-mute", sink, "toggle"])
200
-            (s.sink_name.clone(), s.muted)
171
+            .status()
201
-        };
172
+            .context("Failed to toggle mute")?;
202
 
173
 
203
-        if sink_name.is_empty() {
174
+        if status.success() {
204
-            return Ok(());
175
+            self.state.muted = !self.state.muted;
176
+            debug!("Mute toggled to {}", self.state.muted);
177
+        } else {
178
+            warn!("pactl set-sink-mute failed");
205
         }
179
         }
206
 
180
 
207
-        let mut introspector = context.borrow().introspect();
181
+        Ok(())
208
-        let op = introspector.set_sink_mute_by_name(&sink_name, !muted, None);
182
+    }
183
+
184
+    /// Set mute state
185
+    pub fn set_mute(&mut self, muted: bool) -> Result<()> {
186
+        if !self.available {
187
+            return Ok(());
188
+        }
209
 
189
 
210
-        while op.get_state() == pulse::operation::State::Running {
190
+        let sink = &self.state.sink_name;
211
-            mainloop.borrow_mut().iterate(true);
191
+        if sink.is_empty() {
192
+            return Ok(());
212
         }
193
         }
213
 
194
 
214
-        // Update local state
195
+        let mute_str = if muted { "1" } else { "0" };
215
-        {
196
+
216
-            let mut s = self.state.lock().unwrap();
197
+        let status = Command::new("pactl")
217
-            s.muted = !muted;
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;
218
         }
204
         }
219
 
205
 
220
-        debug!("Mute toggled to {}", !muted);
221
         Ok(())
206
         Ok(())
222
     }
207
     }
223
 
208
 
224
     /// Get current state
209
     /// Get current state
225
     pub fn state(&self) -> VolumeState {
210
     pub fn state(&self) -> VolumeState {
226
-        self.state.lock().unwrap().clone()
211
+        self.state.clone()
227
     }
212
     }
228
 
213
 
229
-    /// Check if connected
214
+    /// Check if available
230
-    pub fn is_connected(&self) -> bool {
215
+    pub fn is_available(&self) -> bool {
231
-        self.connected
216
+        self.available
232
     }
217
     }
233
 }
218
 }
234
 
219
 
235
-impl Drop for VolumeModule {
220
+impl Default for VolumeModule {
236
-    fn drop(&mut self) {
221
+    fn default() -> Self {
237
-        if let Some(context) = &self.context {
222
+        Self::new()
238
-            context.borrow_mut().disconnect();
239
-        }
240
     }
223
     }
241
 }
224
 }