1use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum BacklightMode {
18 None,
20 Automatic,
22 TemporaryManual,
25 PermanentManual,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum BacklightStatus {
36 DisabledBySoftware,
39 DisabledByCriticalBattery,
41 AlsAutomatic,
43 AlsSaturated,
46 TemporaryManual,
48 PermanentManual,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59pub struct BacklightState {
60 pub enabled: bool,
64 pub mode: BacklightMode,
66 pub status: BacklightStatus,
68 pub current_level: u8,
70 pub nb_levels: u8,
72 #[serde(default)]
78 pub power_save: bool,
79 #[serde(default)]
81 pub controls: BacklightControls,
82}
83
84#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
91pub struct BacklightControls {
92 pub automatic: bool,
96 pub level: bool,
99 pub power_save: bool,
101}
102
103impl BacklightControls {
104 #[must_use]
108 pub fn allow(self, change: BacklightChange) -> bool {
109 match change {
110 BacklightChange::Enabled(_) => true,
111 BacklightChange::Automatic(_) => self.automatic,
112 BacklightChange::Level(_) => self.level,
113 BacklightChange::PowerSave(_) => self.power_save,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub enum BacklightChange {
125 Enabled(bool),
127 Automatic(bool),
130 Level(u8),
133 PowerSave(bool),
135}
136
137impl fmt::Display for BacklightChange {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 let on_off = |on: bool| if on { "on" } else { "off" };
140 match *self {
141 Self::Enabled(on) => write!(f, "backlight {}", on_off(on)),
142 Self::Automatic(on) => write!(f, "automatic brightness {}", on_off(on)),
143 Self::Level(level) => write!(f, "backlight level {level}"),
144 Self::PowerSave(on) => write!(f, "battery saving {}", on_off(on)),
145 }
146 }
147}
148
149impl BacklightState {
150 #[must_use]
153 pub fn accepts(self, change: BacklightChange) -> bool {
154 self.controls.allow(change)
155 && match change {
156 BacklightChange::Level(level) => level < self.nb_levels,
157 _ => true,
158 }
159 }
160
161 #[must_use]
165 pub fn is_automatic(self) -> bool {
166 matches!(
167 self.mode,
168 BacklightMode::Automatic | BacklightMode::TemporaryManual
169 )
170 }
171
172 #[must_use]
176 pub fn is_dark(self) -> bool {
177 !self.enabled
178 || self.current_level == 0
179 || matches!(
180 self.status,
181 BacklightStatus::DisabledBySoftware
182 | BacklightStatus::DisabledByCriticalBattery
183 | BacklightStatus::AlsSaturated
184 )
185 }
186
187 #[must_use]
197 pub fn changed(self, change: BacklightChange) -> Self {
198 let mut next = self;
199 match change {
200 BacklightChange::Enabled(on) => next.enabled = on,
201 BacklightChange::Automatic(true) => next.mode = BacklightMode::Automatic,
202 BacklightChange::Automatic(false) => next.mode = BacklightMode::PermanentManual,
204 BacklightChange::Level(level) => {
205 next.mode = BacklightMode::PermanentManual;
206 next.current_level = level;
207 }
208 BacklightChange::PowerSave(on) => next.power_save = on,
209 }
210 next.status = if !next.enabled {
211 BacklightStatus::DisabledBySoftware
212 } else if self.status == BacklightStatus::DisabledByCriticalBattery
213 || (self.enabled && next.mode == self.mode)
214 {
215 self.status
216 } else {
217 match next.mode {
218 BacklightMode::Automatic => BacklightStatus::AlsAutomatic,
219 BacklightMode::TemporaryManual => BacklightStatus::TemporaryManual,
220 BacklightMode::PermanentManual => BacklightStatus::PermanentManual,
221 BacklightMode::None => self.status,
222 }
223 };
224 next
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 fn lit() -> BacklightState {
233 BacklightState {
234 enabled: true,
235 mode: BacklightMode::Automatic,
236 status: BacklightStatus::AlsAutomatic,
237 current_level: 4,
238 nb_levels: 8,
239 power_save: true,
240 controls: BacklightControls {
241 automatic: true,
242 level: true,
243 power_save: true,
244 },
245 }
246 }
247
248 #[test]
249 fn every_change_needs_its_control() {
250 let all = lit();
251 let none = BacklightState {
252 controls: BacklightControls::default(),
253 ..lit()
254 };
255 for change in [
256 BacklightChange::Automatic(false),
257 BacklightChange::Level(3),
258 BacklightChange::PowerSave(false),
259 ] {
260 assert!(all.accepts(change), "{change}");
261 assert!(!none.accepts(change), "{change}");
262 }
263 assert!(none.accepts(BacklightChange::Enabled(false)));
265 }
266
267 #[test]
268 fn a_level_must_be_below_the_level_count() {
269 assert!(lit().accepts(BacklightChange::Level(0)));
270 assert!(lit().accepts(BacklightChange::Level(7)));
271 assert!(!lit().accepts(BacklightChange::Level(8)));
272 }
273
274 #[test]
275 fn the_backlight_keys_leave_the_sensor_in_charge() {
276 let keys = BacklightState {
277 mode: BacklightMode::TemporaryManual,
278 ..lit()
279 };
280 let held = BacklightState {
281 mode: BacklightMode::PermanentManual,
282 ..lit()
283 };
284 assert!(lit().is_automatic());
285 assert!(keys.is_automatic());
286 assert!(!held.is_automatic());
287 }
288
289 #[test]
290 fn changes_name_themselves_for_error_messages() {
291 assert_eq!(BacklightChange::Enabled(true).to_string(), "backlight on");
292 assert_eq!(
293 BacklightChange::Automatic(false).to_string(),
294 "automatic brightness off"
295 );
296 assert_eq!(BacklightChange::Level(3).to_string(), "backlight level 3");
297 assert_eq!(
298 BacklightChange::PowerSave(true).to_string(),
299 "battery saving on"
300 );
301 }
302
303 #[test]
304 fn a_lit_backlight_is_not_dark() {
305 assert!(!lit().is_dark());
306 }
307
308 #[test]
309 fn software_disable_reads_as_dark() {
310 let state = BacklightState {
311 enabled: false,
312 status: BacklightStatus::DisabledBySoftware,
313 ..lit()
314 };
315 assert!(state.is_dark());
316 }
317
318 #[test]
319 fn a_zero_level_reads_as_dark_even_while_enabled() {
320 let state = BacklightState {
321 current_level: 0,
322 ..lit()
323 };
324 assert!(state.is_dark());
325 }
326
327 #[test]
328 fn a_saturated_ambient_sensor_reads_as_dark() {
329 let state = BacklightState {
330 status: BacklightStatus::AlsSaturated,
331 ..lit()
332 };
333 assert!(state.is_dark());
334 }
335
336 #[test]
337 fn turning_the_backlight_off_reads_as_a_software_disable() {
338 let off = lit().changed(BacklightChange::Enabled(false));
339 assert!(!off.enabled);
340 assert_eq!(off.status, BacklightStatus::DisabledBySoftware);
341 assert_eq!(
342 off.mode,
343 BacklightMode::Automatic,
344 "the mode is kept for later"
345 );
346 }
347
348 #[test]
349 fn turning_it_back_on_reports_the_kept_mode() {
350 let off = lit().changed(BacklightChange::Enabled(false));
351 assert_eq!(off.changed(BacklightChange::Enabled(true)), lit());
352 let manual = BacklightState {
353 mode: BacklightMode::PermanentManual,
354 ..off
355 };
356 assert_eq!(
357 manual.changed(BacklightChange::Enabled(true)).status,
358 BacklightStatus::PermanentManual
359 );
360 }
361
362 #[test]
363 fn leaving_automatic_holds_the_level_shown() {
364 let held = lit().changed(BacklightChange::Automatic(false));
365 assert_eq!(held.mode, BacklightMode::PermanentManual);
366 assert_eq!(held.status, BacklightStatus::PermanentManual);
367 assert_eq!(held.current_level, lit().current_level);
368 let back = held.changed(BacklightChange::Automatic(true));
369 assert_eq!(back.mode, BacklightMode::Automatic);
370 assert_eq!(back.status, BacklightStatus::AlsAutomatic);
371 }
372
373 #[test]
374 fn a_level_leaves_automatic_mode() {
375 let set = lit().changed(BacklightChange::Level(2));
376 assert_eq!(set.current_level, 2);
377 assert_eq!(set.mode, BacklightMode::PermanentManual);
378 assert_eq!(set.status, BacklightStatus::PermanentManual);
379 }
380
381 #[test]
382 fn battery_saving_changes_nothing_else() {
383 let saving_off = lit().changed(BacklightChange::PowerSave(false));
384 assert_eq!(
385 saving_off,
386 BacklightState {
387 power_save: false,
388 ..lit()
389 }
390 );
391 }
392
393 #[test]
394 fn firmware_owned_status_is_kept() {
395 let saturated = BacklightState {
396 status: BacklightStatus::AlsSaturated,
397 ..lit()
398 };
399 assert_eq!(
400 saturated.changed(BacklightChange::PowerSave(false)).status,
401 BacklightStatus::AlsSaturated,
402 "the sensor still reads bright"
403 );
404 let critical = BacklightState {
405 status: BacklightStatus::DisabledByCriticalBattery,
406 ..lit()
407 };
408 assert_eq!(
409 critical.changed(BacklightChange::Level(3)).status,
410 BacklightStatus::DisabledByCriticalBattery,
411 "a level does not override the battery shutoff"
412 );
413 assert_eq!(
414 critical.changed(BacklightChange::Enabled(false)).status,
415 BacklightStatus::DisabledBySoftware
416 );
417 }
418}