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