1use zbus::fdo::{DBusProxy, ObjectManagerProxy, PropertiesProxy};
61
62use super::prelude::*;
63use crate::wrappers::RangeMap;
64
65make_log_macro!(debug, "bluetooth");
66
67#[derive(Deserialize, Debug)]
68#[serde(deny_unknown_fields)]
69pub struct Config {
70 pub mac: String,
71 #[serde(default)]
72 pub adapter_mac: Option<String>,
73 #[serde(default)]
74 pub format: FormatConfig,
75 #[serde(default)]
76 pub disconnected_format: FormatConfig,
77 #[serde(default)]
78 pub battery_state: Option<RangeMap<u8, State>>,
79}
80
81pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
82 let mut actions = api.get_actions()?;
83 api.set_default_actions(&[(MouseButton::Right, None, "toggle")])?;
84
85 let format = config.format.with_default(" $icon $name{ $percentage|} ")?;
86 let disconnected_format = config
87 .disconnected_format
88 .with_default(" $icon{ $name|} ")?;
89
90 let mut monitor = DeviceMonitor::new(config.mac.clone(), config.adapter_mac.clone()).await?;
91
92 let battery_states = config.battery_state.clone().unwrap_or_else(|| {
93 vec![
94 (0..=15, State::Critical),
95 (16..=30, State::Warning),
96 (31..=60, State::Info),
97 (61..=100, State::Good),
98 ]
99 .into()
100 });
101
102 loop {
103 match monitor.get_device_info().await {
104 Some(device) => {
106 debug!("Device available, info: {device:?}");
107
108 let mut widget = Widget::new();
109
110 let values = map! {
111 "icon" => Value::icon(device.icon),
112 "name" => Value::text(device.name),
113 "available" => Value::flag(),
114 [if let Some(p) = device.battery_percentage] "percentage" => Value::percents(p),
115 [if let Some(p) = device.battery_percentage]
116 "battery_icon" => Value::icon_progression("bat", p as f64 / 100.0),
117 };
118
119 if device.connected {
120 widget.set_format(format.clone());
121 widget.state = battery_states
122 .get(&device.battery_percentage.unwrap_or(100))
123 .copied()
124 .unwrap_or(State::Good);
125 } else {
126 widget.set_format(disconnected_format.clone());
127 widget.state = State::Idle;
128 }
129
130 widget.set_values(values);
131 api.set_widget(widget)?;
132 }
133 None => {
135 debug!("Showing device as unavailable");
136 let mut widget = Widget::new().with_format(disconnected_format.clone());
137 widget.set_values(map!("icon" => Value::icon("bluetooth")));
138 api.set_widget(widget)?;
139 }
140 }
141
142 loop {
143 select! {
144 res = monitor.wait_for_change() => {
145 res?;
146 break;
147 },
148 Some(action) = actions.recv() => match action.as_ref() {
149 "toggle" => {
150 if let Some(dev) = &monitor.device {
151 if let Ok(connected) = dev.device.connected().await {
152 if connected {
153 let _ = dev.device.disconnect().await;
154 } else {
155 let _ = dev.device.connect().await;
156 }
157 break;
158 }
159 }
160 }
161 _ => (),
162 }
163 }
164 }
165 }
166}
167
168struct DeviceMonitor {
169 mac: String,
170 adapter_mac: Option<String>,
171 manager_proxy: ObjectManagerProxy<'static>,
172 device: Option<Device>,
173}
174
175struct Device {
176 props: PropertiesProxy<'static>,
177 device: Device1Proxy<'static>,
178 battery: Battery1Proxy<'static>,
179}
180
181#[derive(Debug)]
182struct DeviceInfo {
183 connected: bool,
184 icon: &'static str,
185 name: String,
186 battery_percentage: Option<u8>,
187}
188
189impl DeviceMonitor {
190 async fn new(mac: String, adapter_mac: Option<String>) -> Result<Self> {
191 let dbus_conn = new_system_dbus_connection().await?;
192 let manager_proxy = ObjectManagerProxy::builder(&dbus_conn)
193 .destination("org.bluez")
194 .and_then(|x| x.path("/"))
195 .unwrap()
196 .build()
197 .await
198 .error("Failed to create ObjectManagerProxy")?;
199 let device = Device::try_find(&manager_proxy, &mac, adapter_mac.as_deref()).await?;
200 Ok(Self {
201 mac,
202 adapter_mac,
203 manager_proxy,
204 device,
205 })
206 }
207
208 async fn wait_for_change(&mut self) -> Result<()> {
209 match &mut self.device {
210 None => {
211 let mut interface_added = self
212 .manager_proxy
213 .receive_interfaces_added()
214 .await
215 .error("Failed to monitor interfaces")?;
216 loop {
217 interface_added
218 .next()
219 .await
220 .error("Stream ended unexpectedly")?;
221 if let Some(device) = Device::try_find(
222 &self.manager_proxy,
223 &self.mac,
224 self.adapter_mac.as_deref(),
225 )
226 .await?
227 {
228 self.device = Some(device);
229 debug!("Device has been added");
230 return Ok(());
231 }
232 }
233 }
234 Some(device) => {
235 let mut updates = device
236 .props
237 .receive_properties_changed()
238 .await
239 .error("Failed to receive updates")?;
240
241 let mut interface_added = self
242 .manager_proxy
243 .receive_interfaces_added()
244 .await
245 .error("Failed to monitor interfaces")?;
246
247 let mut interface_removed = self
248 .manager_proxy
249 .receive_interfaces_removed()
250 .await
251 .error("Failed to monitor interfaces")?;
252
253 let mut bluez_owner_changed =
254 DBusProxy::new(self.manager_proxy.inner().connection())
255 .await
256 .error("Failed to create DBusProxy")?
257 .receive_name_owner_changed_with_args(&[(0, "org.bluez")])
258 .await
259 .unwrap();
260
261 loop {
262 select! {
263 _ = updates.next_debounced() => {
264 debug!("Got update for device");
265 return Ok(());
266 }
267 Some(event) = interface_added.next() => {
268 let args = event.args().error("Failed to get the args")?;
269 if args.object_path() == device.device.inner().path() {
270 debug!("Interfaces added: {:?}", args.interfaces_and_properties().keys());
271 return Ok(());
272 }
273 }
274 Some(event) = interface_removed.next() => {
275 let args = event.args().error("Failed to get the args")?;
276 if args.object_path() == device.device.inner().path() {
277 self.device = None;
278 debug!("Device is no longer available");
279 return Ok(());
280 }
281 }
282 Some(event) = bluez_owner_changed.next() => {
283 let args = event.args().error("Failed to get the args")?;
284 if args.new_owner.is_none() {
285 self.device = None;
286 debug!("org.bluez disappeared");
287 return Ok(());
288 }
289 }
290 }
291 }
292 }
293 }
294 }
295
296 async fn get_device_info(&mut self) -> Option<DeviceInfo> {
297 let device = self.device.as_ref()?;
298
299 let Ok((connected, name)) =
300 tokio::try_join!(device.device.connected(), device.device.name(),)
301 else {
302 debug!("failed to fetch device info, assuming device or bluez disappeared");
303 self.device = None;
304 return None;
305 };
306
307 let icon: &str = match device.device.icon().await.ok().as_deref() {
309 Some("audio-card" | "audio-headset" | "audio-headphones") => "headphones",
310 Some("input-gaming") => "joystick",
311 Some("input-keyboard") => "keyboard",
312 Some("input-mouse") => "mouse",
313 _ => "bluetooth",
314 };
315
316 Some(DeviceInfo {
317 connected,
318 icon,
319 name,
320 battery_percentage: device.battery.percentage().await.ok(),
321 })
322 }
323}
324
325impl Device {
326 async fn try_find(
327 manager_proxy: &ObjectManagerProxy<'_>,
328 mac: &str,
329 adapter_mac: Option<&str>,
330 ) -> Result<Option<Self>> {
331 let Ok(devices) = manager_proxy.get_managed_objects().await else {
332 debug!("could not get the list of managed objects");
333 return Ok(None);
334 };
335
336 debug!("all managed devices: {:?}", devices);
337
338 let root_object: Option<String> = match adapter_mac {
339 Some(adapter_mac) => {
340 let mut adapter_path = None;
341 for (path, interfaces) in &devices {
342 let adapter_interface = match interfaces.get("org.bluez.Adapter1") {
343 Some(i) => i,
344 None => continue, };
346 let addr: &str = adapter_interface
347 .get("Address")
348 .and_then(|a| a.downcast_ref().ok())
349 .unwrap();
350 if addr == adapter_mac {
351 adapter_path = Some(path);
352 break;
353 }
354 }
355 match adapter_path {
356 Some(path) => Some(format!("{}/", path.as_str())),
357 None => return Ok(None),
358 }
359 }
360 None => None,
361 };
362
363 debug!("root object: {:?}", root_object);
364
365 for (path, interfaces) in devices {
366 if let Some(root) = &root_object {
367 if !path.starts_with(root) {
368 continue;
369 }
370 }
371
372 let Some(device_interface) = interfaces.get("org.bluez.Device1") else {
373 continue;
375 };
376
377 let addr: &str = device_interface
378 .get("Address")
379 .and_then(|a| a.downcast_ref().ok())
380 .unwrap();
381 if addr != mac {
382 continue;
383 }
384
385 debug!("Found device with path {:?}", path);
386
387 return Ok(Some(Self {
388 props: PropertiesProxy::builder(manager_proxy.inner().connection())
389 .destination("org.bluez")
390 .and_then(|x| x.path(path.clone()))
391 .unwrap()
392 .build()
393 .await
394 .error("Failed to create PropertiesProxy")?,
395 device: Device1Proxy::builder(manager_proxy.inner().connection())
396 .cache_properties(zbus::proxy::CacheProperties::No)
398 .path(path.clone())
399 .unwrap()
400 .build()
401 .await
402 .error("Failed to create Device1Proxy")?,
403 battery: Battery1Proxy::builder(manager_proxy.inner().connection())
404 .cache_properties(zbus::proxy::CacheProperties::No)
405 .path(path)
406 .unwrap()
407 .build()
408 .await
409 .error("Failed to create Battery1Proxy")?,
410 }));
411 }
412
413 debug!("No device found");
414 Ok(None)
415 }
416}
417
418#[zbus::proxy(interface = "org.bluez.Device1", default_service = "org.bluez")]
419trait Device1 {
420 fn connect(&self) -> zbus::Result<()>;
421 fn disconnect(&self) -> zbus::Result<()>;
422
423 #[zbus(property)]
424 fn connected(&self) -> zbus::Result<bool>;
425
426 #[zbus(property)]
427 fn name(&self) -> zbus::Result<String>;
428
429 #[zbus(property)]
430 fn icon(&self) -> zbus::Result<String>;
431}
432
433#[zbus::proxy(interface = "org.bluez.Battery1", default_service = "org.bluez")]
434trait Battery1 {
435 #[zbus(property)]
436 fn percentage(&self) -> zbus::Result<u8>;
437}