1#[cfg(test)]
5mod tests;
6
7use std::cell::RefCell;
8use std::rc::Rc;
9use std::sync::Once;
10use std::time::Duration;
11
12use crabshell_reactive::{ReadSignal, Signal};
13use crabshell_ui::Cx;
14use zbus::{Array, Connection, Message, OwnedObjectPath, OwnedVariant};
15
16use crate::Shared;
17use crate::dbus::{self, CALL_TIMEOUT, Daemon, Props, set_changed};
18
19const UPOWER: &str = "org.freedesktop.UPower";
20const UPOWER_PATH: &str = "/org/freedesktop/UPower";
21const DEVICE: &str = "org.freedesktop.UPower.Device";
22const PROFILES: &str = "net.hadess.PowerProfiles";
24const PROFILES_PATH: &str = "/net/hadess/PowerProfiles";
25
26const MAX_DEVICES: usize = 64;
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub enum DeviceKind {
33 #[default]
35 Unknown,
36 LinePower,
38 Battery,
40 Ups,
42 Monitor,
44 Mouse,
46 Keyboard,
48 Phone,
50 Tablet,
52 GamingInput,
54 Headset,
56 Headphones,
58 Speakers,
60}
61
62#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
64pub enum BatteryState {
65 #[default]
67 Unknown,
68 Charging,
70 Discharging,
72 Empty,
74 FullyCharged,
76 PendingCharge,
78 PendingDischarge,
80}
81
82#[derive(Clone, Debug, Default, PartialEq)]
84pub struct PowerDevice {
85 pub path: String,
87 pub kind: DeviceKind,
89 pub model: String,
91 pub icon_name: String,
93 pub present: bool,
95 pub power_supply: bool,
97 pub state: BatteryState,
99 pub percent: f64,
101 pub time_to_empty: Option<Duration>,
103 pub time_to_full: Option<Duration>,
105 pub energy_rate: f64,
107}
108
109impl PowerDevice {
110 fn from_props(path: String, props: &Props) -> PowerDevice {
111 let seconds = |name| {
112 dbus::get::<i64>(props, name)
113 .filter(|seconds| *seconds > 0)
114 .map(|seconds| Duration::from_secs(seconds as u64))
115 };
116 PowerDevice {
117 path,
118 kind: match dbus::get::<u32>(props, "Type") {
119 Some(1) => DeviceKind::LinePower,
120 Some(2) => DeviceKind::Battery,
121 Some(3) => DeviceKind::Ups,
122 Some(4) => DeviceKind::Monitor,
123 Some(5) => DeviceKind::Mouse,
124 Some(6) => DeviceKind::Keyboard,
125 Some(8) => DeviceKind::Phone,
126 Some(10) => DeviceKind::Tablet,
127 Some(12) => DeviceKind::GamingInput,
128 Some(17) => DeviceKind::Headset,
129 Some(18) => DeviceKind::Speakers,
130 Some(19) => DeviceKind::Headphones,
131 _ => DeviceKind::Unknown,
132 },
133 model: dbus::text(props, "Model"),
134 icon_name: dbus::text(props, "IconName"),
135 present: dbus::get(props, "IsPresent").unwrap_or(false),
136 power_supply: dbus::get(props, "PowerSupply").unwrap_or(false),
137 state: match dbus::get::<u32>(props, "State") {
138 Some(1) => BatteryState::Charging,
139 Some(2) => BatteryState::Discharging,
140 Some(3) => BatteryState::Empty,
141 Some(4) => BatteryState::FullyCharged,
142 Some(5) => BatteryState::PendingCharge,
143 Some(6) => BatteryState::PendingDischarge,
144 _ => BatteryState::Unknown,
145 },
146 percent: dbus::get::<f64>(props, "Percentage")
147 .filter(|percent| percent.is_finite())
148 .map_or(0.0, |percent| percent.clamp(0.0, 100.0)),
149 time_to_empty: seconds("TimeToEmpty"),
150 time_to_full: seconds("TimeToFull"),
151 energy_rate: dbus::get::<f64>(props, "EnergyRate")
152 .filter(|rate| rate.is_finite())
153 .unwrap_or(0.0),
154 }
155 }
156}
157
158#[derive(Clone)]
161pub struct UPower {
162 devices: ReadSignal<Vec<PowerDevice>>,
163 display: ReadSignal<Option<PowerDevice>>,
164 on_battery: ReadSignal<bool>,
165}
166
167impl UPower {
168 pub fn devices(&self) -> ReadSignal<Vec<PowerDevice>> {
171 self.devices
172 }
173
174 pub fn display_device(&self) -> ReadSignal<Option<PowerDevice>> {
178 self.display
179 }
180
181 pub fn on_battery(&self) -> ReadSignal<bool> {
183 self.on_battery
184 }
185}
186
187pub(crate) fn upower(cx: &Cx) -> UPower {
189 let Some(service) = cx.service::<UPowerService>() else {
190 static LOGGED: Once = Once::new();
191 LOGGED.call_once(|| {
192 tracing::warn!("cx.upower() needs the services the shell provides; it stays empty");
193 });
194 return UPower {
195 devices: cx.signal(Vec::new()).read_only(),
196 display: cx.signal(None).read_only(),
197 on_battery: cx.signal(false).read_only(),
198 };
199 };
200 service.start()
201}
202
203pub(crate) struct UPowerService {
205 shared: Rc<Shared>,
206 state: RefCell<Option<UPowerState>>,
207}
208
209struct UPowerState {
210 devices: Signal<Vec<PowerDevice>>,
211 display: Signal<Option<PowerDevice>>,
212 on_battery: Signal<bool>,
213 objects: Vec<(String, Props)>,
215 display_device: Option<(String, Props)>,
217 generation: u64,
218}
219
220impl UPowerService {
221 pub(crate) fn new(shared: Rc<Shared>) -> UPowerService {
222 UPowerService {
223 shared,
224 state: RefCell::new(None),
225 }
226 }
227
228 fn start(self: &Rc<Self>) -> UPower {
229 if let Some(state) = &*self.state.borrow() {
230 return UPower {
231 devices: state.devices.read_only(),
232 display: state.display.read_only(),
233 on_battery: state.on_battery.read_only(),
234 };
235 }
236 let root = &self.shared.root;
237 let state = UPowerState {
238 devices: root.signal(Vec::new()),
239 display: root.signal(None),
240 on_battery: root.signal(false),
241 objects: Vec::new(),
242 display_device: None,
243 generation: 0,
244 };
245 let upower = UPower {
246 devices: state.devices.read_only(),
247 display: state.display.read_only(),
248 on_battery: state.on_battery.read_only(),
249 };
250 *self.state.borrow_mut() = Some(state);
251 match self.shared.config.system_bus.clone() {
252 Some(address) => {
253 let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
254 self.shared.spawn(follow);
255 }
256 None => tracing::info!("no system bus, so UPower's devices stay empty"),
257 }
258 upower
259 }
260
261 fn publish(&self) {
263 let shown = self.state.borrow().as_ref().map(|state| {
265 let devices = state
266 .objects
267 .iter()
268 .map(|(path, props)| PowerDevice::from_props(path.clone(), props))
269 .collect::<Vec<_>>();
270 let display = state
271 .display_device
272 .as_ref()
273 .map(|(path, props)| PowerDevice::from_props(path.clone(), props));
274 (state.devices, devices, state.display, display)
275 });
276 if let Some((devices_signal, devices, display_signal, display)) = shown {
277 set_changed(devices_signal, devices);
278 set_changed(display_signal, display);
279 }
280 }
281}
282
283impl Daemon for UPowerService {
284 const NAME: &'static str = UPOWER;
285
286 fn alive(&self) -> bool {
287 let state = self.state.borrow();
288 state
289 .as_ref()
290 .is_some_and(|state| state.devices.try_with(|_| ()).is_some())
291 }
292
293 fn clear(&self) -> u64 {
294 let (devices, display, on_battery, generation) = {
295 let mut state = self.state.borrow_mut();
296 let Some(state) = state.as_mut() else {
297 return 0;
298 };
299 state.objects.clear();
300 state.display_device = None;
301 state.generation += 1;
302 (
303 state.devices,
304 state.display,
305 state.on_battery,
306 state.generation,
307 )
308 };
309 set_changed(devices, Vec::new());
310 set_changed(display, None);
311 set_changed(on_battery, false);
312 generation
313 }
314
315 async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
316 let shared = &self.shared;
317 let read = async {
318 let root = dbus::get_all(shared, &connection, UPOWER, UPOWER_PATH, UPOWER).await?;
319 let call = connection.call_method(
320 Some(UPOWER),
321 UPOWER_PATH,
322 Some(UPOWER),
323 "EnumerateDevices",
324 &(),
325 );
326 let paths: Vec<OwnedObjectPath> = shared
327 .timeout(CALL_TIMEOUT, call)
328 .await?
329 .body()
330 .deserialize()?;
331 let call = connection.call_method(
332 Some(UPOWER),
333 UPOWER_PATH,
334 Some(UPOWER),
335 "GetDisplayDevice",
336 &(),
337 );
338 let display: OwnedObjectPath = shared
339 .timeout(CALL_TIMEOUT, call)
340 .await?
341 .body()
342 .deserialize()?;
343 let mut objects = Vec::new();
344 for path in paths.into_iter().take(MAX_DEVICES) {
345 let path = path.as_str().to_owned();
346 match dbus::get_all(shared, &connection, UPOWER, &path, DEVICE).await {
347 Ok(props) => objects.push((path, props)),
348 Err(error) => tracing::info!(%error, path, "couldn't read a power device"),
350 }
351 }
352 let display_path = display.as_str().to_owned();
353 let display = dbus::get_all(shared, &connection, UPOWER, &display_path, DEVICE)
354 .await
355 .ok()
356 .map(|props| (display_path, props));
357 zbus::Result::Ok((root, objects, display))
358 };
359 let (root, objects, display) = match read.await {
360 Ok(read) => read,
361 Err(error) => return tracing::warn!(%error, "couldn't read UPower"),
362 };
363 let on_battery = {
364 let mut state = self.state.borrow_mut();
365 let Some(state) = state
366 .as_mut()
367 .filter(|state| state.generation == generation)
368 else {
369 return;
370 };
371 state.objects = objects;
372 state.display_device = display;
373 state.on_battery
374 };
375 set_changed(on_battery, dbus::get(&root, "OnBattery").unwrap_or(false));
376 self.publish();
377 }
378
379 fn reads_again(&self, message: &Message) -> bool {
381 let header = message.header();
382 header.interface().map(|interface| interface.as_str()) == Some(UPOWER)
383 && matches!(
384 header.member().map(|member| member.as_str()),
385 Some("DeviceAdded" | "DeviceRemoved")
386 )
387 }
388
389 fn signal(self: &Rc<Self>, _: &Connection, message: &Message) {
390 let Some((path, interface, changed, dropped)) = dbus::properties_changed(message) else {
391 return;
392 };
393 {
394 if path == UPOWER_PATH && interface == UPOWER {
395 let on_battery = self.state.borrow().as_ref().map(|state| state.on_battery);
396 if let (Some(signal), Some(value)) = (on_battery, changed.get("OnBattery")) {
397 set_changed(signal, bool::try_from(value).unwrap_or(false));
398 }
399 return;
400 }
401 if interface != DEVICE {
402 return;
403 }
404 {
405 let mut state = self.state.borrow_mut();
406 let Some(state) = state.as_mut() else {
407 return;
408 };
409 let display = state.display_device.iter_mut();
410 let object = state
411 .objects
412 .iter_mut()
413 .chain(display)
414 .find(|(at, _)| *at == path);
415 let Some((_, props)) = object else {
416 return;
417 };
418 dbus::apply(props, changed, dropped);
419 }
420 }
421 self.publish();
422 }
423}
424
425#[derive(Clone, Copy, Debug, PartialEq, Eq)]
427pub enum PowerProfile {
428 PowerSaver,
430 Balanced,
432 Performance,
434}
435
436impl PowerProfile {
437 fn parse(name: &str) -> Option<PowerProfile> {
438 match name {
439 "power-saver" => Some(PowerProfile::PowerSaver),
440 "balanced" => Some(PowerProfile::Balanced),
441 "performance" => Some(PowerProfile::Performance),
442 _ => None,
443 }
444 }
445
446 fn name(self) -> &'static str {
447 match self {
448 PowerProfile::PowerSaver => "power-saver",
449 PowerProfile::Balanced => "balanced",
450 PowerProfile::Performance => "performance",
451 }
452 }
453}
454
455#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
458pub enum PerformanceDegradation {
459 #[default]
462 None,
463 LapDetected,
465 HighTemperature,
467 Other,
470}
471
472impl PerformanceDegradation {
473 fn parse(reason: &str) -> PerformanceDegradation {
474 match reason {
475 "" => PerformanceDegradation::None,
476 "lap-detected" => PerformanceDegradation::LapDetected,
477 "high-operating-temperature" => PerformanceDegradation::HighTemperature,
478 other => {
479 tracing::debug!(
480 reason = other,
481 "power-profiles-daemon names an unknown reason for degraded performance"
482 );
483 PerformanceDegradation::Other
484 }
485 }
486 }
487}
488
489#[derive(Clone)]
491pub struct PowerProfiles {
492 service: Option<Rc<ProfilesService>>,
493 active: ReadSignal<Option<PowerProfile>>,
494 available: ReadSignal<Vec<PowerProfile>>,
495 degradation: ReadSignal<PerformanceDegradation>,
496}
497
498impl PowerProfiles {
499 pub fn active(&self) -> ReadSignal<Option<PowerProfile>> {
502 self.active
503 }
504
505 pub fn available(&self) -> ReadSignal<Vec<PowerProfile>> {
508 self.available
509 }
510
511 pub fn degradation(&self) -> ReadSignal<PerformanceDegradation> {
514 self.degradation
515 }
516
517 pub fn set(&self, profile: PowerProfile) {
523 let Some(service) = &self.service else {
524 return;
525 };
526 let Some(connection) = service
527 .state
528 .borrow()
529 .as_ref()
530 .and_then(|s| s.connection.clone())
531 else {
532 return tracing::info!("power-profiles-daemon isn't running; no profile set");
533 };
534 let shared = Rc::clone(&service.shared);
535 service.shared.spawn(async move {
536 let value = zbus::Variant::from(profile.name());
537 let at = (PROFILES_PATH, PROFILES);
538 let set = dbus::set(&shared, &connection, PROFILES, at, "ActiveProfile", value);
539 if let Err(error) = set.await {
540 tracing::warn!(%error, profile = profile.name(), "the power profile wasn't set");
541 }
542 });
543 }
544}
545
546pub(crate) fn power_profiles(cx: &Cx) -> PowerProfiles {
548 let Some(service) = cx.service::<ProfilesService>() else {
549 static LOGGED: Once = Once::new();
550 LOGGED.call_once(|| {
551 tracing::warn!("cx.power_profiles() needs the services the shell provides; it stays empty");
552 });
553 return PowerProfiles {
554 service: None,
555 active: cx.signal(None).read_only(),
556 available: cx.signal(Vec::new()).read_only(),
557 degradation: cx.signal(PerformanceDegradation::None).read_only(),
558 };
559 };
560 service.start()
561}
562
563pub(crate) struct ProfilesService {
565 shared: Rc<Shared>,
566 state: RefCell<Option<ProfilesState>>,
567}
568
569struct ProfilesState {
570 active: Signal<Option<PowerProfile>>,
571 available: Signal<Vec<PowerProfile>>,
572 degradation: Signal<PerformanceDegradation>,
573 props: Props,
574 connection: Option<Connection>,
576 generation: u64,
577}
578
579impl ProfilesService {
580 pub(crate) fn new(shared: Rc<Shared>) -> ProfilesService {
581 ProfilesService {
582 shared,
583 state: RefCell::new(None),
584 }
585 }
586
587 fn start(self: &Rc<Self>) -> PowerProfiles {
588 if let Some(state) = &*self.state.borrow() {
589 return PowerProfiles {
590 service: Some(Rc::clone(self)),
591 active: state.active.read_only(),
592 available: state.available.read_only(),
593 degradation: state.degradation.read_only(),
594 };
595 }
596 let root = &self.shared.root;
597 let (active, available) = (root.signal(None), root.signal(Vec::new()));
598 let degradation = root.signal(PerformanceDegradation::None);
599 *self.state.borrow_mut() = Some(ProfilesState {
600 active,
601 available,
602 degradation,
603 props: Props::new(),
604 connection: None,
605 generation: 0,
606 });
607 match self.shared.config.system_bus.clone() {
608 Some(address) => {
609 let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
610 self.shared.spawn(follow);
611 }
612 None => tracing::info!("no system bus, so there is no power profile"),
613 }
614 PowerProfiles {
615 service: Some(Rc::clone(self)),
616 active: active.read_only(),
617 available: available.read_only(),
618 degradation: degradation.read_only(),
619 }
620 }
621
622 fn publish(&self) {
623 let shown = self.state.borrow().as_ref().map(|state| {
624 let active = dbus::get::<&str>(&state.props, "ActiveProfile").and_then(PowerProfile::parse);
625 let available = state.props.get("Profiles").map(profiles);
626 let degradation = dbus::get::<&str>(&state.props, "PerformanceDegraded")
627 .map_or(PerformanceDegradation::None, PerformanceDegradation::parse);
628 (
629 (state.active, active),
630 (state.available, available.unwrap_or_default()),
631 (state.degradation, degradation),
632 )
633 });
634 if let Some((active, available, degradation)) = shown {
635 set_changed(active.0, active.1);
636 set_changed(available.0, available.1);
637 set_changed(degradation.0, degradation.1);
638 }
639 }
640}
641
642fn profiles(value: &OwnedVariant) -> Vec<PowerProfile> {
645 let Ok(list) = <&Array>::try_from(value) else {
646 return Vec::new();
647 };
648 list
649 .inner()
650 .iter()
651 .filter_map(|entry| {
652 let zbus::Variant::Dict(dict) = entry else {
653 return None;
654 };
655 let name = dict.iter().find_map(|(key, value)| {
656 if <&str>::try_from(key).ok()? != "Profile" {
657 return None;
658 }
659 let value = match value {
660 zbus::Variant::Variant(inner) => &**inner,
661 other => other,
662 };
663 <&str>::try_from(value).ok()
664 })?;
665 PowerProfile::parse(name)
666 })
667 .collect()
668}
669
670impl Daemon for ProfilesService {
671 const NAME: &'static str = PROFILES;
672
673 fn alive(&self) -> bool {
674 let state = self.state.borrow();
675 state
676 .as_ref()
677 .is_some_and(|state| state.active.try_with(|_| ()).is_some())
678 }
679
680 fn clear(&self) -> u64 {
681 let generation = {
682 let mut state = self.state.borrow_mut();
683 let Some(state) = state.as_mut() else {
684 return 0;
685 };
686 state.props.clear();
687 state.connection = None;
688 state.generation += 1;
689 state.generation
690 };
691 self.publish();
692 generation
693 }
694
695 async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
696 let read = dbus::get_all(&self.shared, &connection, PROFILES, PROFILES_PATH, PROFILES).await;
697 let props = match read {
698 Ok(props) => props,
699 Err(error) => return tracing::warn!(%error, "couldn't read power-profiles-daemon"),
700 };
701 {
702 let mut state = self.state.borrow_mut();
703 let Some(state) = state
704 .as_mut()
705 .filter(|state| state.generation == generation)
706 else {
707 return;
708 };
709 state.props = props;
710 state.connection = Some(connection);
711 }
712 self.publish();
713 }
714
715 fn signal(self: &Rc<Self>, _: &Connection, message: &Message) {
716 let Some((path, interface, changed, dropped)) = dbus::properties_changed(message) else {
717 return;
718 };
719 if path != PROFILES_PATH || interface != PROFILES {
720 return;
721 }
722 if let Some(state) = self.state.borrow_mut().as_mut() {
723 dbus::apply(&mut state.props, changed, dropped);
724 }
725 self.publish();
726 }
727}