1use super::prelude::*;
53
54mod battery;
55mod connectivity_report;
56use battery::BatteryDbusProxy;
57use connectivity_report::ConnectivityDbusProxy;
58
59make_log_macro!(debug, "kdeconnect");
60
61#[derive(Deserialize, Debug, SmartDefault)]
62#[serde(deny_unknown_fields, default)]
63pub struct Config {
64 pub device_id: Option<String>,
65 pub format: FormatConfig,
66 pub disconnected_format: FormatConfig,
67 pub missing_format: FormatConfig,
68 #[default(60)]
69 pub bat_good: u8,
70 #[default(60)]
71 pub bat_info: u8,
72 #[default(30)]
73 pub bat_warning: u8,
74 #[default(15)]
75 pub bat_critical: u8,
76}
77
78pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
79 let format = config
80 .format
81 .with_default(" $icon $name {$bat_icon $bat_charge |}{$notif_icon |}")?;
82 let disconnected_format = config.disconnected_format.with_default(" $icon ")?;
83 let missing_format = config.missing_format.with_default(" $icon x ")?;
84
85 let battery_state = (
86 config.bat_good,
87 config.bat_info,
88 config.bat_warning,
89 config.bat_critical,
90 ) != (0, 0, 0, 0);
91
92 let mut monitor = DeviceMonitor::new(config.device_id.clone()).await?;
93
94 loop {
95 match monitor.get_device_info().await {
96 Some(info) => {
97 let mut widget = Widget::new();
98 if info.connected {
99 widget.set_format(format.clone());
100 } else {
101 widget.set_format(disconnected_format.clone());
102 }
103
104 let mut values = map! {
105 [if info.connected] "icon" => Value::icon("phone"),
106 [if !info.connected] "icon" => Value::icon("phone_disconnected"),
107 [if let Some(name) = info.name] "name" => Value::text(name),
108 [if info.notifications > 0] "notif_count" => Value::number(info.notifications),
109 [if info.notifications > 0] "notif_icon" => Value::icon("notification"),
110 [if let Some(bat) = info.bat_level] "bat_charge" => Value::percents(bat),
111 };
112
113 if let Some(bat_level) = info.bat_level {
114 values.insert(
115 "bat_icon".into(),
116 Value::icon_progression(
117 if info.charging { "bat_charging" } else { "bat" },
118 bat_level as f64 / 100.0,
119 ),
120 );
121 if battery_state {
122 widget.state = if info.charging {
123 State::Good
124 } else if bat_level <= config.bat_critical {
125 State::Critical
126 } else if bat_level <= config.bat_info {
127 State::Info
128 } else if bat_level > config.bat_good {
129 State::Good
130 } else {
131 State::Idle
132 };
133 }
134 }
135
136 if !battery_state {
137 widget.state = if info.notifications == 0 {
138 State::Idle
139 } else {
140 State::Info
141 };
142 }
143
144 if let Some(cellular_network_type) = info.cellular_network_type {
145 let cell_network_percent =
149 (info.cellular_network_strength.clamp(0, 4) * 25) as f64;
150 values.insert(
151 "network_icon".into(),
152 Value::icon_progression(
153 "net_cellular",
154 (info.cellular_network_strength + 1).clamp(0, 5) as f64 / 5.0,
155 ),
156 );
157 values.insert(
158 "network_strength".into(),
159 Value::percents(cell_network_percent),
160 );
161
162 if info.cellular_network_strength <= 0 {
163 widget.state = State::Critical;
164 values.insert("network_type".into(), Value::text("×".into()));
165 } else {
166 values.insert("network_type".into(), Value::text(cellular_network_type));
167 }
168 }
169
170 widget.set_values(values);
171 api.set_widget(widget)?;
172 }
173 None => {
174 let mut widget = Widget::new().with_format(missing_format.clone());
175 widget.set_values(map! { "icon" => Value::icon("phone_disconnected") });
176 api.set_widget(widget)?;
177 }
178 }
179
180 monitor.wait_for_change().await?;
181 }
182}
183
184struct DeviceMonitor {
185 device_id: Option<String>,
186 daemon_proxy: DaemonDbusProxy<'static>,
187 device: Option<Device>,
188}
189
190struct Device {
191 id: String,
192 device_proxy: DeviceDbusProxy<'static>,
193 battery_proxy: BatteryDbusProxy<'static>,
194 notifications_proxy: NotificationsDbusProxy<'static>,
195 connectivity_proxy: ConnectivityDbusProxy<'static>,
196 device_signals: zbus::proxy::SignalStream<'static>,
197 notifications_signals: zbus::proxy::SignalStream<'static>,
198 battery_refreshed: battery::refreshedStream,
199 connectivity_refreshed: connectivity_report::refreshedStream,
200}
201
202struct DeviceInfo {
203 connected: bool,
204 name: Option<String>,
205 notifications: usize,
206 charging: bool,
207 bat_level: Option<u8>,
208 cellular_network_type: Option<String>,
209 cellular_network_strength: i32,
210}
211
212impl DeviceMonitor {
213 async fn new(device_id: Option<String>) -> Result<Self> {
214 let dbus_conn = new_dbus_connection().await?;
215 let daemon_proxy = DaemonDbusProxy::new(&dbus_conn)
216 .await
217 .error("Failed to create DaemonDbusProxy")?;
218 let device = Device::try_find(&daemon_proxy, device_id.as_deref()).await?;
219 Ok(Self {
220 device_id,
221 daemon_proxy,
222 device,
223 })
224 }
225
226 async fn wait_for_change(&mut self) -> Result<()> {
227 match &mut self.device {
228 None => {
229 let mut device_added = self
230 .daemon_proxy
231 .receive_device_added()
232 .await
233 .error("Couldn't create stream")?;
234 loop {
235 device_added
236 .next()
237 .await
238 .error("Stream ended unexpectedly")?;
239 if let Some(device) =
240 Device::try_find(&self.daemon_proxy, self.device_id.as_deref()).await?
241 {
242 self.device = Some(device);
243 return Ok(());
244 }
245 }
246 }
247 Some(dev) => {
248 let mut device_removed = self
249 .daemon_proxy
250 .receive_device_removed()
251 .await
252 .error("Couldn't create stream")?;
253 loop {
254 select! {
255 rem = device_removed.next() => {
256 let rem = rem.error("stream ended unexpectedly")?;
257 let args = rem.args().error("dbus error")?;
258 if args.id() == &dev.id {
259 self.device = Device::try_find(&self.daemon_proxy, self.device_id.as_deref()).await?;
260 return Ok(());
261 }
262 }
263 _ = dev.wait_for_change() => {
264 if !dev.connected().await {
265 debug!("device became unreachable, re-searching");
266 if let Some(dev) = Device::try_find(&self.daemon_proxy, self.device_id.as_deref()).await?
267 && dev.connected().await {
268 debug!("selected {:?}", dev.id);
269 self.device = Some(dev);
270 }
271 }
272 return Ok(())
273 }
274 }
275 }
276 }
277 }
278 }
279
280 async fn get_device_info(&mut self) -> Option<DeviceInfo> {
281 let device = self.device.as_ref()?;
282 let (bat_level, charging) = device.battery().await;
283 let (cellular_network_type, cellular_network_strength) = device.network().await;
284 Some(DeviceInfo {
285 connected: device.connected().await,
286 name: device.name().await,
287 notifications: device.notifications().await,
288 charging,
289 bat_level,
290 cellular_network_type,
291 cellular_network_strength,
292 })
293 }
294}
295
296impl Device {
297 async fn try_find(
299 daemon_proxy: &DaemonDbusProxy<'_>,
300 device_id: Option<&str>,
301 ) -> Result<Option<Self>> {
302 let Ok(mut devices) = daemon_proxy.devices().await else {
303 debug!("could not get the list of managed objects");
304 return Ok(None);
305 };
306
307 debug!("all devices: {:?}", devices);
308
309 if let Some(device_id) = device_id {
310 devices.retain(|id| id == device_id);
311 }
312
313 let mut selected_device = None;
314
315 for id in devices {
316 let device_proxy = DeviceDbusProxy::builder(daemon_proxy.inner().connection())
317 .cache_properties(zbus::proxy::CacheProperties::No)
318 .path(format!("/modules/kdeconnect/devices/{id}"))
319 .unwrap()
320 .build()
321 .await
322 .error("Failed to create DeviceDbusProxy")?;
323 let reachable = device_proxy.is_reachable().await.unwrap_or(false);
324 selected_device = Some((id, device_proxy));
325 if reachable {
326 break;
327 }
328 }
329
330 let Some((device_id, device_proxy)) = selected_device else {
331 debug!("No device found");
332 return Ok(None);
333 };
334
335 let device_path = format!("/modules/kdeconnect/devices/{device_id}");
336 let battery_path = format!("{device_path}/battery");
337 let notifications_path = format!("{device_path}/notifications");
338 let connectivity_path = format!("{device_path}/connectivity_report");
339
340 let battery_proxy = BatteryDbusProxy::builder(daemon_proxy.inner().connection())
341 .cache_properties(zbus::proxy::CacheProperties::No)
342 .path(battery_path)
343 .error("Failed to set battery path")?
344 .build()
345 .await
346 .error("Failed to create BatteryDbusProxy")?;
347 let notifications_proxy =
348 NotificationsDbusProxy::builder(daemon_proxy.inner().connection())
349 .cache_properties(zbus::proxy::CacheProperties::No)
350 .path(notifications_path)
351 .error("Failed to set notifications path")?
352 .build()
353 .await
354 .error("Failed to create BatteryDbusProxy")?;
355 let connectivity_proxy = ConnectivityDbusProxy::builder(daemon_proxy.inner().connection())
356 .cache_properties(zbus::proxy::CacheProperties::No)
357 .path(connectivity_path)
358 .error("Failed to set connectivity path")?
359 .build()
360 .await
361 .error("Failed to create ConnectivityDbusProxy")?;
362
363 let device_signals = device_proxy
364 .inner()
365 .receive_all_signals()
366 .await
367 .error("Failed to receive signals")?;
368 let notifications_signals = notifications_proxy
369 .inner()
370 .receive_all_signals()
371 .await
372 .error("Failed to receive signals")?;
373 let battery_refreshed = battery_proxy
374 .receive_refreshed()
375 .await
376 .error("Failed to receive signals")?;
377 let connectivity_refreshed = connectivity_proxy
378 .receive_refreshed()
379 .await
380 .error("Failed to receive signals")?;
381
382 Ok(Some(Self {
383 id: device_id,
384 device_proxy,
385 battery_proxy,
386 notifications_proxy,
387 connectivity_proxy,
388 device_signals,
389 notifications_signals,
390 battery_refreshed,
391 connectivity_refreshed,
392 }))
393 }
394
395 async fn wait_for_change(&mut self) {
396 select! {
397 _ = self.device_signals.next() => (),
398 _ = self.notifications_signals.next() => (),
399 _ = self.battery_refreshed.next() => (),
400 _ = self.connectivity_refreshed.next() => (),
401 }
402 }
403
404 async fn connected(&self) -> bool {
405 self.device_proxy.is_reachable().await.unwrap_or(false)
406 }
407
408 async fn name(&self) -> Option<String> {
409 self.device_proxy.name().await.ok()
410 }
411
412 async fn battery(&self) -> (Option<u8>, bool) {
413 let (charge, is_charging) = tokio::join!(
414 self.battery_proxy.charge(),
415 self.battery_proxy.is_charging(),
416 );
417 (
418 charge.ok().map(|x| x.clamp(0, 100) as u8),
419 is_charging.unwrap_or(false),
420 )
421 }
422
423 async fn notifications(&self) -> usize {
424 self.notifications_proxy
425 .active_notifications()
426 .await
427 .map(|n| n.len())
428 .unwrap_or(0)
429 }
430
431 async fn network(&self) -> (Option<String>, i32) {
432 let (ty, strength) = tokio::join!(
433 self.connectivity_proxy.cellular_network_type(),
434 self.connectivity_proxy.cellular_network_strength(),
435 );
436 (ty.ok(), strength.unwrap_or(-1))
437 }
438}
439
440#[zbus::proxy(
441 interface = "org.kde.kdeconnect.daemon",
442 default_service = "org.kde.kdeconnect",
443 default_path = "/modules/kdeconnect"
444)]
445trait DaemonDbus {
446 #[zbus(name = "devices")]
447 fn devices(&self) -> zbus::Result<Vec<String>>;
448
449 #[zbus(signal, name = "deviceAdded")]
450 fn device_added(&self, id: String) -> zbus::Result<()>;
451
452 #[zbus(signal, name = "deviceRemoved")]
453 fn device_removed(&self, id: String) -> zbus::Result<()>;
454}
455
456#[zbus::proxy(
457 interface = "org.kde.kdeconnect.device",
458 default_service = "org.kde.kdeconnect"
459)]
460trait DeviceDbus {
461 #[zbus(property, name = "isReachable")]
462 fn is_reachable(&self) -> zbus::Result<bool>;
463
464 #[zbus(signal, name = "reachableChanged")]
465 fn reachable_changed(&self, reachable: bool) -> zbus::Result<()>;
466
467 #[zbus(property, name = "name")]
468 fn name(&self) -> zbus::Result<String>;
469
470 #[zbus(signal, name = "nameChanged")]
471 fn name_changed_(&self, name: &str) -> zbus::Result<()>;
472}
473
474#[zbus::proxy(
475 interface = "org.kde.kdeconnect.device.notifications",
476 default_service = "org.kde.kdeconnect"
477)]
478trait NotificationsDbus {
479 #[zbus(name = "activeNotifications")]
480 fn active_notifications(&self) -> zbus::Result<Vec<String>>;
481
482 #[zbus(signal, name = "allNotificationsRemoved")]
483 fn all_notifications_removed(&self) -> zbus::Result<()>;
484
485 #[zbus(signal, name = "notificationPosted")]
486 fn notification_posted(&self, id: &str) -> zbus::Result<()>;
487
488 #[zbus(signal, name = "notificationRemoved")]
489 fn notification_removed(&self, id: &str) -> zbus::Result<()>;
490}