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 if dev.connected().await {
268 debug!("selected {:?}", dev.id);
269 self.device = Some(dev);
270 }
271 }
272 }
273 return Ok(())
274 }
275 }
276 }
277 }
278 }
279 }
280
281 async fn get_device_info(&mut self) -> Option<DeviceInfo> {
282 let device = self.device.as_ref()?;
283 let (bat_level, charging) = device.battery().await;
284 let (cellular_network_type, cellular_network_strength) = device.network().await;
285 Some(DeviceInfo {
286 connected: device.connected().await,
287 name: device.name().await,
288 notifications: device.notifications().await,
289 charging,
290 bat_level,
291 cellular_network_type,
292 cellular_network_strength,
293 })
294 }
295}
296
297impl Device {
298 async fn try_find(
300 daemon_proxy: &DaemonDbusProxy<'_>,
301 device_id: Option<&str>,
302 ) -> Result<Option<Self>> {
303 let Ok(mut devices) = daemon_proxy.devices().await else {
304 debug!("could not get the list of managed objects");
305 return Ok(None);
306 };
307
308 debug!("all devices: {:?}", devices);
309
310 if let Some(device_id) = device_id {
311 devices.retain(|id| id == device_id);
312 }
313
314 let mut selected_device = None;
315
316 for id in devices {
317 let device_proxy = DeviceDbusProxy::builder(daemon_proxy.inner().connection())
318 .cache_properties(zbus::proxy::CacheProperties::No)
319 .path(format!("/modules/kdeconnect/devices/{id}"))
320 .unwrap()
321 .build()
322 .await
323 .error("Failed to create DeviceDbusProxy")?;
324 let reachable = device_proxy.is_reachable().await.unwrap_or(false);
325 selected_device = Some((id, device_proxy));
326 if reachable {
327 break;
328 }
329 }
330
331 let Some((device_id, device_proxy)) = selected_device else {
332 debug!("No device found");
333 return Ok(None);
334 };
335
336 let device_path = format!("/modules/kdeconnect/devices/{device_id}");
337 let battery_path = format!("{device_path}/battery");
338 let notifications_path = format!("{device_path}/notifications");
339 let connectivity_path = format!("{device_path}/connectivity_report");
340
341 let battery_proxy = BatteryDbusProxy::builder(daemon_proxy.inner().connection())
342 .cache_properties(zbus::proxy::CacheProperties::No)
343 .path(battery_path)
344 .error("Failed to set battery path")?
345 .build()
346 .await
347 .error("Failed to create BatteryDbusProxy")?;
348 let notifications_proxy =
349 NotificationsDbusProxy::builder(daemon_proxy.inner().connection())
350 .cache_properties(zbus::proxy::CacheProperties::No)
351 .path(notifications_path)
352 .error("Failed to set notifications path")?
353 .build()
354 .await
355 .error("Failed to create BatteryDbusProxy")?;
356 let connectivity_proxy = ConnectivityDbusProxy::builder(daemon_proxy.inner().connection())
357 .cache_properties(zbus::proxy::CacheProperties::No)
358 .path(connectivity_path)
359 .error("Failed to set connectivity path")?
360 .build()
361 .await
362 .error("Failed to create ConnectivityDbusProxy")?;
363
364 let device_signals = device_proxy
365 .inner()
366 .receive_all_signals()
367 .await
368 .error("Failed to receive signals")?;
369 let notifications_signals = notifications_proxy
370 .inner()
371 .receive_all_signals()
372 .await
373 .error("Failed to receive signals")?;
374 let battery_refreshed = battery_proxy
375 .receive_refreshed()
376 .await
377 .error("Failed to receive signals")?;
378 let connectivity_refreshed = connectivity_proxy
379 .receive_refreshed()
380 .await
381 .error("Failed to receive signals")?;
382
383 Ok(Some(Self {
384 id: device_id,
385 device_proxy,
386 battery_proxy,
387 notifications_proxy,
388 connectivity_proxy,
389 device_signals,
390 notifications_signals,
391 battery_refreshed,
392 connectivity_refreshed,
393 }))
394 }
395
396 async fn wait_for_change(&mut self) {
397 select! {
398 _ = self.device_signals.next() => (),
399 _ = self.notifications_signals.next() => (),
400 _ = self.battery_refreshed.next() => (),
401 _ = self.connectivity_refreshed.next() => (),
402 }
403 }
404
405 async fn connected(&self) -> bool {
406 self.device_proxy.is_reachable().await.unwrap_or(false)
407 }
408
409 async fn name(&self) -> Option<String> {
410 self.device_proxy.name().await.ok()
411 }
412
413 async fn battery(&self) -> (Option<u8>, bool) {
414 let (charge, is_charging) = tokio::join!(
415 self.battery_proxy.charge(),
416 self.battery_proxy.is_charging(),
417 );
418 (
419 charge.ok().map(|x| x.clamp(0, 100) as u8),
420 is_charging.unwrap_or(false),
421 )
422 }
423
424 async fn notifications(&self) -> usize {
425 self.notifications_proxy
426 .active_notifications()
427 .await
428 .map(|n| n.len())
429 .unwrap_or(0)
430 }
431
432 async fn network(&self) -> (Option<String>, i32) {
433 let (ty, strength) = tokio::join!(
434 self.connectivity_proxy.cellular_network_type(),
435 self.connectivity_proxy.cellular_network_strength(),
436 );
437 (ty.ok(), strength.unwrap_or(-1))
438 }
439}
440
441#[zbus::proxy(
442 interface = "org.kde.kdeconnect.daemon",
443 default_service = "org.kde.kdeconnect",
444 default_path = "/modules/kdeconnect"
445)]
446trait DaemonDbus {
447 #[zbus(name = "devices")]
448 fn devices(&self) -> zbus::Result<Vec<String>>;
449
450 #[zbus(signal, name = "deviceAdded")]
451 fn device_added(&self, id: String) -> zbus::Result<()>;
452
453 #[zbus(signal, name = "deviceRemoved")]
454 fn device_removed(&self, id: String) -> zbus::Result<()>;
455}
456
457#[zbus::proxy(
458 interface = "org.kde.kdeconnect.device",
459 default_service = "org.kde.kdeconnect"
460)]
461trait DeviceDbus {
462 #[zbus(property, name = "isReachable")]
463 fn is_reachable(&self) -> zbus::Result<bool>;
464
465 #[zbus(signal, name = "reachableChanged")]
466 fn reachable_changed(&self, reachable: bool) -> zbus::Result<()>;
467
468 #[zbus(property, name = "name")]
469 fn name(&self) -> zbus::Result<String>;
470
471 #[zbus(signal, name = "nameChanged")]
472 fn name_changed_(&self, name: &str) -> zbus::Result<()>;
473}
474
475#[zbus::proxy(
476 interface = "org.kde.kdeconnect.device.notifications",
477 default_service = "org.kde.kdeconnect"
478)]
479trait NotificationsDbus {
480 #[zbus(name = "activeNotifications")]
481 fn active_notifications(&self) -> zbus::Result<Vec<String>>;
482
483 #[zbus(signal, name = "allNotificationsRemoved")]
484 fn all_notifications_removed(&self) -> zbus::Result<()>;
485
486 #[zbus(signal, name = "notificationPosted")]
487 fn notification_posted(&self, id: &str) -> zbus::Result<()>;
488
489 #[zbus(signal, name = "notificationRemoved")]
490 fn notification_removed(&self, id: &str) -> zbus::Result<()>;
491}