Skip to main content

kobo_core/
capabilities.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Nayeem Bin Ahsan
3//! Capability detection (plan S5): drives the read-aloud vs plain-reader state
4//! machine. `MockCapabilities` is for the desktop simulator; the real impl
5//! (`KoboCapabilities`) lives in the `kobo` backend crate (checks network reach
6//! + A2DP sink presence).
7
8/// Device-state query surface. Implemented concretely by `KoboCapabilities`
9/// (reads sysfs/dbus) and by `MockCapabilities` (scripted for desktop). Routing
10/// device queries through this trait lets the reader's status-refresh and
11/// play-enabled decisions be unit-tested without hardware.
12pub trait Capabilities {
13    fn network_available(&self) -> bool;
14    fn audio_sink_available(&self) -> bool;
15
16    fn read_aloud_available(&self) -> bool {
17        self.network_available() && self.audio_sink_available()
18    }
19
20    fn battery_pct(&self) -> i32 {
21        0
22    }
23    fn wifi_name(&self) -> Option<String> {
24        None
25    }
26    fn bt_name(&self) -> Option<String> {
27        None
28    }
29    fn current_clock(&self) -> String {
30        "--:--".to_string()
31    }
32}
33
34#[derive(Debug, Clone, Default)]
35pub struct MockCapabilities {
36    pub network: bool,
37    pub audio_sink: bool,
38    pub battery: i32,
39    pub wifi_ssid: Option<String>,
40    pub bt_device: Option<String>,
41    pub clock: String,
42}
43
44impl Capabilities for MockCapabilities {
45    fn network_available(&self) -> bool {
46        self.network
47    }
48    fn audio_sink_available(&self) -> bool {
49        self.audio_sink
50    }
51    fn battery_pct(&self) -> i32 {
52        self.battery
53    }
54    fn wifi_name(&self) -> Option<String> {
55        self.wifi_ssid.clone()
56    }
57    fn bt_name(&self) -> Option<String> {
58        self.bt_device.clone()
59    }
60    fn current_clock(&self) -> String {
61        if self.clock.is_empty() {
62            "--:--".to_string()
63        } else {
64            self.clock.clone()
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn state_machine() {
75        assert!(MockCapabilities {
76            network: true,
77            audio_sink: true,
78            ..Default::default()
79        }
80        .read_aloud_available());
81        assert!(!MockCapabilities {
82            network: false,
83            audio_sink: true,
84            ..Default::default()
85        }
86        .read_aloud_available());
87        assert!(!MockCapabilities {
88            network: true,
89            audio_sink: false,
90            ..Default::default()
91        }
92        .read_aloud_available());
93        assert!(!MockCapabilities {
94            network: false,
95            audio_sink: false,
96            ..Default::default()
97        }
98        .read_aloud_available());
99    }
100
101    #[test]
102    fn mock_battery_and_names_round_trip() {
103        let mut m = MockCapabilities::default();
104        m.battery = 73;
105        m.wifi_ssid = Some("Net".into());
106        m.bt_device = Some("Speaker".into());
107        m.clock = "9:41 PM".into();
108        assert_eq!(m.battery_pct(), 73);
109        assert_eq!(m.wifi_name().as_deref(), Some("Net"));
110        assert_eq!(m.bt_name().as_deref(), Some("Speaker"));
111        assert_eq!(m.current_clock(), "9:41 PM");
112    }
113
114    #[test]
115    fn mock_defaults_are_benign() {
116        let m = MockCapabilities::default();
117        assert_eq!(m.battery_pct(), 0);
118        assert!(m.wifi_name().is_none());
119        assert!(m.bt_name().is_none());
120        assert_eq!(m.current_clock(), "--:--");
121    }
122}