use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BacklightMode {
None,
Automatic,
TemporaryManual,
PermanentManual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BacklightStatus {
DisabledBySoftware,
DisabledByCriticalBattery,
AlsAutomatic,
AlsSaturated,
TemporaryManual,
PermanentManual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BacklightState {
pub enabled: bool,
pub mode: BacklightMode,
pub status: BacklightStatus,
pub current_level: u8,
pub nb_levels: u8,
#[serde(default)]
pub power_save: bool,
#[serde(default)]
pub controls: BacklightControls,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BacklightControls {
pub automatic: bool,
pub level: bool,
pub power_save: bool,
}
impl BacklightControls {
#[must_use]
pub fn allow(self, change: BacklightChange) -> bool {
match change {
BacklightChange::Enabled(_) => true,
BacklightChange::Automatic(_) => self.automatic,
BacklightChange::Level(_) => self.level,
BacklightChange::PowerSave(_) => self.power_save,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BacklightChange {
Enabled(bool),
Automatic(bool),
Level(u8),
PowerSave(bool),
}
impl fmt::Display for BacklightChange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let on_off = |on: bool| if on { "on" } else { "off" };
match *self {
Self::Enabled(on) => write!(f, "backlight {}", on_off(on)),
Self::Automatic(on) => write!(f, "automatic brightness {}", on_off(on)),
Self::Level(level) => write!(f, "backlight level {level}"),
Self::PowerSave(on) => write!(f, "battery saving {}", on_off(on)),
}
}
}
impl BacklightState {
#[must_use]
pub fn accepts(self, change: BacklightChange) -> bool {
self.controls.allow(change)
&& match change {
BacklightChange::Level(level) => level < self.nb_levels,
_ => true,
}
}
#[must_use]
pub fn is_automatic(self) -> bool {
matches!(
self.mode,
BacklightMode::Automatic | BacklightMode::TemporaryManual
)
}
#[must_use]
pub fn is_dark(self) -> bool {
!self.enabled
|| self.current_level == 0
|| matches!(
self.status,
BacklightStatus::DisabledBySoftware
| BacklightStatus::DisabledByCriticalBattery
| BacklightStatus::AlsSaturated
)
}
#[must_use]
pub fn changed(self, change: BacklightChange) -> Self {
let mut next = self;
match change {
BacklightChange::Enabled(on) => next.enabled = on,
BacklightChange::Automatic(true) => next.mode = BacklightMode::Automatic,
BacklightChange::Automatic(false) => next.mode = BacklightMode::PermanentManual,
BacklightChange::Level(level) => {
next.mode = BacklightMode::PermanentManual;
next.current_level = level;
}
BacklightChange::PowerSave(on) => next.power_save = on,
}
next.status = if !next.enabled {
BacklightStatus::DisabledBySoftware
} else if self.status == BacklightStatus::DisabledByCriticalBattery
|| (self.enabled && next.mode == self.mode)
{
self.status
} else {
match next.mode {
BacklightMode::Automatic => BacklightStatus::AlsAutomatic,
BacklightMode::TemporaryManual => BacklightStatus::TemporaryManual,
BacklightMode::PermanentManual => BacklightStatus::PermanentManual,
BacklightMode::None => self.status,
}
};
next
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lit() -> BacklightState {
BacklightState {
enabled: true,
mode: BacklightMode::Automatic,
status: BacklightStatus::AlsAutomatic,
current_level: 4,
nb_levels: 8,
power_save: true,
controls: BacklightControls {
automatic: true,
level: true,
power_save: true,
},
}
}
#[test]
fn every_change_needs_its_control() {
let all = lit();
let none = BacklightState {
controls: BacklightControls::default(),
..lit()
};
for change in [
BacklightChange::Automatic(false),
BacklightChange::Level(3),
BacklightChange::PowerSave(false),
] {
assert!(all.accepts(change), "{change}");
assert!(!none.accepts(change), "{change}");
}
assert!(none.accepts(BacklightChange::Enabled(false)));
}
#[test]
fn a_level_must_be_below_the_level_count() {
assert!(lit().accepts(BacklightChange::Level(0)));
assert!(lit().accepts(BacklightChange::Level(7)));
assert!(!lit().accepts(BacklightChange::Level(8)));
}
#[test]
fn the_backlight_keys_leave_the_sensor_in_charge() {
let keys = BacklightState {
mode: BacklightMode::TemporaryManual,
..lit()
};
let held = BacklightState {
mode: BacklightMode::PermanentManual,
..lit()
};
assert!(lit().is_automatic());
assert!(keys.is_automatic());
assert!(!held.is_automatic());
}
#[test]
fn changes_name_themselves_for_error_messages() {
assert_eq!(BacklightChange::Enabled(true).to_string(), "backlight on");
assert_eq!(
BacklightChange::Automatic(false).to_string(),
"automatic brightness off"
);
assert_eq!(BacklightChange::Level(3).to_string(), "backlight level 3");
assert_eq!(
BacklightChange::PowerSave(true).to_string(),
"battery saving on"
);
}
#[test]
fn a_lit_backlight_is_not_dark() {
assert!(!lit().is_dark());
}
#[test]
fn software_disable_reads_as_dark() {
let state = BacklightState {
enabled: false,
status: BacklightStatus::DisabledBySoftware,
..lit()
};
assert!(state.is_dark());
}
#[test]
fn a_zero_level_reads_as_dark_even_while_enabled() {
let state = BacklightState {
current_level: 0,
..lit()
};
assert!(state.is_dark());
}
#[test]
fn a_saturated_ambient_sensor_reads_as_dark() {
let state = BacklightState {
status: BacklightStatus::AlsSaturated,
..lit()
};
assert!(state.is_dark());
}
#[test]
fn turning_the_backlight_off_reads_as_a_software_disable() {
let off = lit().changed(BacklightChange::Enabled(false));
assert!(!off.enabled);
assert_eq!(off.status, BacklightStatus::DisabledBySoftware);
assert_eq!(
off.mode,
BacklightMode::Automatic,
"the mode is kept for later"
);
}
#[test]
fn turning_it_back_on_reports_the_kept_mode() {
let off = lit().changed(BacklightChange::Enabled(false));
assert_eq!(off.changed(BacklightChange::Enabled(true)), lit());
let manual = BacklightState {
mode: BacklightMode::PermanentManual,
..off
};
assert_eq!(
manual.changed(BacklightChange::Enabled(true)).status,
BacklightStatus::PermanentManual
);
}
#[test]
fn leaving_automatic_holds_the_level_shown() {
let held = lit().changed(BacklightChange::Automatic(false));
assert_eq!(held.mode, BacklightMode::PermanentManual);
assert_eq!(held.status, BacklightStatus::PermanentManual);
assert_eq!(held.current_level, lit().current_level);
let back = held.changed(BacklightChange::Automatic(true));
assert_eq!(back.mode, BacklightMode::Automatic);
assert_eq!(back.status, BacklightStatus::AlsAutomatic);
}
#[test]
fn a_level_leaves_automatic_mode() {
let set = lit().changed(BacklightChange::Level(2));
assert_eq!(set.current_level, 2);
assert_eq!(set.mode, BacklightMode::PermanentManual);
assert_eq!(set.status, BacklightStatus::PermanentManual);
}
#[test]
fn battery_saving_changes_nothing_else() {
let saving_off = lit().changed(BacklightChange::PowerSave(false));
assert_eq!(
saving_off,
BacklightState {
power_save: false,
..lit()
}
);
}
#[test]
fn firmware_owned_status_is_kept() {
let saturated = BacklightState {
status: BacklightStatus::AlsSaturated,
..lit()
};
assert_eq!(
saturated.changed(BacklightChange::PowerSave(false)).status,
BacklightStatus::AlsSaturated,
"the sensor still reads bright"
);
let critical = BacklightState {
status: BacklightStatus::DisabledByCriticalBattery,
..lit()
};
assert_eq!(
critical.changed(BacklightChange::Level(3)).status,
BacklightStatus::DisabledByCriticalBattery,
"a level does not override the battery shutoff"
);
assert_eq!(
critical.changed(BacklightChange::Enabled(false)).status,
BacklightStatus::DisabledBySoftware
);
}
}