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 && 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
167struct DeviceMonitor {
168 mac: String,
169 adapter_mac: Option<String>,
170 manager_proxy: ObjectManagerProxy<'static>,
171 device: Option<Device>,
172}
173
174struct Device {
175 props: PropertiesProxy<'static>,
176 device: Device1Proxy<'static>,
177 battery: Battery1Proxy<'static>,
178}
179
180#[derive(Debug)]
181struct DeviceInfo {
182 connected: bool,
183 icon: &'static str,
184 name: String,
185 battery_percentage: Option<u8>,
186}
187
188impl DeviceMonitor {
189 async fn new(mac: String, adapter_mac: Option<String>) -> Result<Self> {
190 let dbus_conn = new_system_dbus_connection().await?;
191 let manager_proxy = ObjectManagerProxy::builder(&dbus_conn)
192 .destination("org.bluez")
193 .and_then(|x| x.path("/"))
194 .unwrap()
195 .build()
196 .await
197 .error("Failed to create ObjectManagerProxy")?;
198 let device = Device::try_find(&manager_proxy, &mac, adapter_mac.as_deref()).await?;
199 Ok(Self {
200 mac,
201 adapter_mac,
202 manager_proxy,
203 device,
204 })
205 }
206
207 async fn wait_for_change(&mut self) -> Result<()> {
208 match &mut self.device {
209 None => {
210 let mut interface_added = self
211 .manager_proxy
212 .receive_interfaces_added()
213 .await
214 .error("Failed to monitor interfaces")?;
215 loop {
216 interface_added
217 .next()
218 .await
219 .error("Stream ended unexpectedly")?;
220 if let Some(device) = Device::try_find(
221 &self.manager_proxy,
222 &self.mac,
223 self.adapter_mac.as_deref(),
224 )
225 .await?
226 {
227 self.device = Some(device);
228 debug!("Device has been added");
229 return Ok(());
230 }
231 }
232 }
233 Some(device) => {
234 let mut updates = device
235 .props
236 .receive_properties_changed()
237 .await
238 .error("Failed to receive updates")?;
239
240 let mut interface_added = self
241 .manager_proxy
242 .receive_interfaces_added()
243 .await
244 .error("Failed to monitor interfaces")?;
245
246 let mut interface_removed = self
247 .manager_proxy
248 .receive_interfaces_removed()
249 .await
250 .error("Failed to monitor interfaces")?;
251
252 let mut bluez_owner_changed =
253 DBusProxy::new(self.manager_proxy.inner().connection())
254 .await
255 .error("Failed to create DBusProxy")?
256 .receive_name_owner_changed_with_args(&[(0, "org.bluez")])
257 .await
258 .unwrap();
259
260 loop {
261 select! {
262 _ = updates.next_debounced() => {
263 debug!("Got update for device");
264 return Ok(());
265 }
266 Some(event) = interface_added.next() => {
267 let args = event.args().error("Failed to get the args")?;
268 if args.object_path() == device.device.inner().path() {
269 debug!("Interfaces added: {:?}", args.interfaces_and_properties().keys());
270 return Ok(());
271 }
272 }
273 Some(event) = interface_removed.next() => {
274 let args = event.args().error("Failed to get the args")?;
275 if args.object_path() == device.device.inner().path() {
276 self.device = None;
277 debug!("Device is no longer available");
278 return Ok(());
279 }
280 }
281 Some(event) = bluez_owner_changed.next() => {
282 let args = event.args().error("Failed to get the args")?;
283 if args.new_owner.is_none() {
284 self.device = None;
285 debug!("org.bluez disappeared");
286 return Ok(());
287 }
288 }
289 }
290 }
291 }
292 }
293 }
294
295 async fn get_device_info(&mut self) -> Option<DeviceInfo> {
296 let device = self.device.as_ref()?;
297
298 let Ok((connected, name)) =
299 tokio::try_join!(device.device.connected(), device.device.name(),)
300 else {
301 debug!("failed to fetch device info, assuming device or bluez disappeared");
302 self.device = None;
303 return None;
304 };
305
306 let icon: &str = match device.device.icon().await.ok().as_deref() {
308 Some("audio-card" | "audio-headset" | "audio-headphones") => "headphones",
309 Some("input-gaming") => "joystick",
310 Some("input-keyboard") => "keyboard",
311 Some("input-mouse") => "mouse",
312 _ => "bluetooth",
313 };
314
315 Some(DeviceInfo {
316 connected,
317 icon,
318 name,
319 battery_percentage: device.battery.percentage().await.ok(),
320 })
321 }
322}
323
324impl Device {
325 async fn try_find(
326 manager_proxy: &ObjectManagerProxy<'_>,
327 mac: &str,
328 adapter_mac: Option<&str>,
329 ) -> Result<Option<Self>> {
330 let Ok(devices) = manager_proxy.get_managed_objects().await else {
331 debug!("could not get the list of managed objects");
332 return Ok(None);
333 };
334
335 debug!("all managed devices: {:?}", devices);
336
337 let root_object: Option<String> = match adapter_mac {
338 Some(adapter_mac) => {
339 let mut adapter_path = None;
340 for (path, interfaces) in &devices {
341 let adapter_interface = match interfaces.get("org.bluez.Adapter1") {
342 Some(i) => i,
343 None => continue, };
345 let addr: &str = adapter_interface
346 .get("Address")
347 .and_then(|a| a.downcast_ref().ok())
348 .unwrap();
349 if addr == adapter_mac {
350 adapter_path = Some(path);
351 break;
352 }
353 }
354 match adapter_path {
355 Some(path) => Some(format!("{}/", path.as_str())),
356 None => return Ok(None),
357 }
358 }
359 None => None,
360 };
361
362 debug!("root object: {:?}", root_object);
363
364 for (path, interfaces) in devices {
365 if let Some(root) = &root_object
366 && !path.starts_with(root)
367 {
368 continue;
369 }
370
371 let Some(device_interface) = interfaces.get("org.bluez.Device1") else {
372 continue;
374 };
375
376 let addr: &str = device_interface
377 .get("Address")
378 .and_then(|a| a.downcast_ref().ok())
379 .unwrap();
380 if addr != mac {
381 continue;
382 }
383
384 debug!("Found device with path {:?}", path);
385
386 return Ok(Some(Self {
387 props: PropertiesProxy::builder(manager_proxy.inner().connection())
388 .destination("org.bluez")
389 .and_then(|x| x.path(path.clone()))
390 .unwrap()
391 .build()
392 .await
393 .error("Failed to create PropertiesProxy")?,
394 device: Device1Proxy::builder(manager_proxy.inner().connection())
395 .cache_properties(zbus::proxy::CacheProperties::No)
397 .path(path.clone())
398 .unwrap()
399 .build()
400 .await
401 .error("Failed to create Device1Proxy")?,
402 battery: Battery1Proxy::builder(manager_proxy.inner().connection())
403 .cache_properties(zbus::proxy::CacheProperties::No)
404 .path(path)
405 .unwrap()
406 .build()
407 .await
408 .error("Failed to create Battery1Proxy")?,
409 }));
410 }
411
412 debug!("No device found");
413 Ok(None)
414 }
415}
416
417#[zbus::proxy(interface = "org.bluez.Device1", default_service = "org.bluez")]
418trait Device1 {
419 fn connect(&self) -> zbus::Result<()>;
420 fn disconnect(&self) -> zbus::Result<()>;
421
422 #[zbus(property)]
423 fn connected(&self) -> zbus::Result<bool>;
424
425 #[zbus(property)]
426 fn name(&self) -> zbus::Result<String>;
427
428 #[zbus(property)]
429 fn icon(&self) -> zbus::Result<String>;
430}
431
432#[zbus::proxy(interface = "org.bluez.Battery1", default_service = "org.bluez")]
433trait Battery1 {
434 #[zbus(property)]
435 fn percentage(&self) -> zbus::Result<u8>;
436}