use std::{
borrow::Cow,
fs::File,
marker::PhantomData,
path::{Path, PathBuf},
str::FromStr,
};
use crate::{
err::{Error, ErrorKind},
private, utils, Light,
};
#[cfg(not(test))]
pub const LEDDIR: &str = "/sys/class/leds";
#[cfg(test)]
pub const LEDDIR: &str = "testbldir";
#[derive(Debug)]
pub enum LedType {
Dimmable(Led<Dimmable>),
NonDimmable(Led<NonDimmable>),
}
#[derive(Debug)]
pub struct Dimmable;
#[derive(Debug)]
pub struct NonDimmable;
#[derive(Debug)]
pub struct Led<Type> {
name: LedName<'static>,
max: u8,
current: u8,
path: PathBuf,
brightness: File,
marker: PhantomData<Type>,
}
impl Led<()> {
pub fn new(name: Cow<str>) -> crate::Result<LedType> {
Self::new_inner(LedName::parse(name), None)
}
#[cfg(feature = "locking")]
pub fn new_locked(name: Cow<str>, blocking: bool) -> crate::Result<LedType> {
Self::new_inner(
LedName::parse(name),
Some(if blocking {
utils::Lock::Blocking
} else {
utils::Lock::NonBlocking
}),
)
}
fn new_inner(name: LedName, lock: Option<utils::Lock>) -> crate::Result<LedType> {
let utils::Info {
current,
max,
brightness,
path,
} = utils::read_info(LEDDIR, &name.raw, lock)?;
#[allow(clippy::cast_possible_truncation)]
let (max, current) = (max as _, current as _);
let name = name.into_owned();
let led = if max == 1 {
LedType::NonDimmable(Led {
name,
max,
current,
path,
brightness,
marker: PhantomData,
})
} else {
LedType::Dimmable(Led {
name,
max,
current,
path,
brightness,
marker: PhantomData,
})
};
Ok(led)
}
pub fn from_name(name: LedName) -> crate::Result<LedType> {
Self::new_inner(name, None)
}
}
impl<Type> Led<Type> {
pub fn color(&self) -> Color {
self.name.color
}
pub fn function(&self) -> Function {
self.name.function
}
pub fn parsed_name(&self) -> Option<&str> {
self.name.parsed_name()
}
}
impl<Type> private::Sealed for Led<Type> {}
impl super::Dimmable for Led<Dimmable> {}
impl super::Toggleable for Led<Dimmable> {}
impl super::Toggleable for Led<NonDimmable> {}
impl<Type> Light for Led<Type> {
type Value = u8;
fn name(&self) -> &str {
self.name.raw_name()
}
fn current(&self) -> Self::Value {
self.current
}
fn max(&self) -> Self::Value {
self.max
}
#[doc(hidden)]
fn set_current(&mut self, _: crate::private::Internal, current: Self::Value) {
self.current = current;
}
#[doc(hidden)]
fn brightness_file(&mut self, _: crate::private::Internal) -> &mut File {
&mut self.brightness
}
fn device_path(&self) -> &Path {
&self.path
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LedName<'a> {
raw: Cow<'a, str>,
len: usize,
color: Color,
function: Function,
}
impl<'a> LedName<'a> {
pub fn parse(name: Cow<'a, str>) -> Self {
let mut name = Self {
raw: name,
len: 0,
color: Color::default(),
function: Function::default(),
};
let Some((rem, fun)) = name.raw.rsplit_once(':') else {
return name;
};
let Ok(fun): Result<Function, _> = fun.parse();
name.function = fun;
let Some((rem, clr)) = rem.rsplit_once(':') else {
return name;
};
let Ok(clr): Result<Color, _> = clr.parse();
name.color = clr;
name.len = rem.len();
name
}
pub fn color(&self) -> Color {
self.color
}
pub fn function(&self) -> Function {
self.function
}
pub fn raw_name(&self) -> &str {
&self.raw
}
pub fn parsed_name(&self) -> Option<&str> {
(self.len != 0).then_some(&self.raw[..self.len])
}
pub fn initialize(self) -> crate::Result<LedType> {
Led::from_name(self)
}
pub fn into_owned(self) -> LedName<'static> {
LedName {
raw: self.raw.into_owned().into(),
..self
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Color {
White = 0,
Red = 1,
Green = 2,
Blue = 3,
Amber = 4,
Violet = 5,
Yellow = 6,
Ir = 7,
Multi = 8,
Rgb = 9,
Purple = 10,
Orange = 11,
Pink = 12,
Cyan = 13,
Lime = 14,
Max = 15,
#[default]
Unknown,
}
impl FromStr for Color {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let clr = match s {
"white" => Color::White,
"red" => Color::Red,
"green" => Color::Green,
"blue" => Color::Blue,
"amber" => Color::Amber,
"violet" => Color::Violet,
"yellow" => Color::Yellow,
"ir" => Color::Ir,
"multi" => Color::Multi,
"rgb" => Color::Rgb,
"purple" => Color::Purple,
"orange" => Color::Orange,
"pink" => Color::Pink,
"cyan" => Color::Cyan,
"lime" => Color::Lime,
"max" => Color::Max,
_ => Color::default(),
};
Ok(clr)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Function {
Capslock,
Scrolllock,
Numlock,
Fnlock,
KbdBacklight,
Power,
Disk,
Charging,
Status,
Micmute,
Mute,
Player1,
Player2,
Player3,
Player4,
Player5,
Activity,
Alarm,
Backlight,
Bluetooth,
Boot,
Cpu,
Debug,
DiskActivity,
DiskErr,
DiskRead,
DiskWrite,
Fault,
Flash,
Heartbeat,
Indicator,
Lan,
Mail,
Mobile,
Mtd,
Panic,
Programming,
Rx,
Sd,
SpeedLan,
SpeedWan,
Standby,
Torch,
Tx,
Usb,
Wan,
WanOnline,
Wlan,
Wlan2ghz,
Wlan5ghz,
Wlan6ghz,
Wps,
#[default]
Unknown,
}
impl FromStr for Function {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
#[allow(clippy::enum_glob_use)]
use Function::*;
let func = match s {
"capslock" => Capslock,
"scrolllock" => Scrolllock,
"numlock" => Numlock,
"fnlock" => Fnlock,
"kbd_backlight" => KbdBacklight,
"power" => Power,
"disk" => Disk,
"charging" => Charging,
"status" => Status,
"micmute" => Micmute,
"mute" => Mute,
"player-1" => Player1,
"player-2" => Player2,
"player-3" => Player3,
"player-4" => Player4,
"player-5" => Player5,
"activity" => Activity,
"alarm" => Alarm,
"backlight" => Backlight,
"bluetooth" => Bluetooth,
"boot" => Boot,
"cpu" => Cpu,
"debug" => Debug,
"disk-activity" => DiskActivity,
"disk-err" => DiskErr,
"disk-read" => DiskRead,
"disk-write" => DiskWrite,
"fault" => Fault,
"flash" => Flash,
"heartbeat" => Heartbeat,
"indicator" => Indicator,
"lan" => Lan,
"mail" => Mail,
"mobile" => Mobile,
"mtd" => Mtd,
"panic" => Panic,
"programming" => Programming,
"rx" => Rx,
"sd" => Sd,
"speed-lan" => SpeedLan,
"speed-wan" => SpeedWan,
"standby" => Standby,
"torch" => Torch,
"tx" => Tx,
"usb" => Usb,
"wan" => Wan,
"wan-online" => WanOnline,
"wlan" => Wlan,
"wlan-2ghz" => Wlan2ghz,
"wlan-5ghz" => Wlan5ghz,
"wlan-6ghz" => Wlan6ghz,
"wps" => Wps,
_ => Unknown,
};
Ok(func)
}
}
pub fn led_names() -> crate::Result<Vec<LedName<'static>>> {
let read_dir_err = |err| Error::from(ErrorKind::ReadDir { dir: LEDDIR }).with_source(err);
let mut names = vec![];
for d in std::fs::read_dir(LEDDIR).map_err(read_dir_err)? {
let is_dir = d.as_ref().is_ok_and(|inr| inr.path().is_dir());
if is_dir {
let entry = d.map_err(read_dir_err)?;
names.push(LedName::parse(entry.file_name().to_string_lossy()).into_owned());
}
}
Ok(names)
}
pub fn leds() -> crate::Result<Vec<LedType>> {
led_names().and_then(|names| names.into_iter().map(Led::from_name).collect())
}
pub fn leds_from_names<'a>(
names: impl IntoIterator<Item = LedName<'a>>,
) -> crate::Result<Vec<LedType>> {
names.into_iter().map(Led::from_name).collect()
}
pub fn set_led_state(led_name: &str, state: bool) -> crate::Result<()> {
match Led::new(led_name.into())? {
LedType::Dimmable(mut led) => led.write_value(if state { led.max() } else { 0 }),
LedType::NonDimmable(mut led) => led.write_value(u8::from(state)),
}
}
pub fn get_led_state(led_name: &str) -> crate::Result<bool> {
let state = match Led::new(led_name.into())? {
LedType::Dimmable(led) => led.current(),
LedType::NonDimmable(led) => led.current(),
};
Ok(state != 0)
}
pub fn set_led_value(led_name: &str, value: u8) -> crate::Result<()> {
match Led::new(led_name.into())? {
LedType::Dimmable(mut led) => led.write_value(value),
LedType::NonDimmable(mut led) => led.write_value(value),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{clean_up, setup_test_env};
#[test]
fn parse_name() {
let cases = [
(
"platform:white:kbd_backlight",
LedName {
raw: "platform:white:kbd_backlight".into(),
len: 8,
color: Color::White,
function: Function::KbdBacklight,
},
Some("platform"),
),
(
"input13::capslock",
LedName {
raw: "input13::capslock".into(),
len: 7,
color: Color::Unknown,
function: Function::Capslock,
},
Some("input13"),
),
(
"input7::numlock",
LedName {
raw: "input7::numlock".into(),
len: 6,
color: Color::Unknown,
function: Function::Numlock,
},
Some("input7"),
),
(
"name::",
LedName {
raw: "name::".into(),
len: 4,
color: Color::Unknown,
function: Function::Unknown,
},
Some("name"),
),
(
"unknown",
LedName {
raw: "unknown".into(),
len: 0,
color: Color::Unknown,
function: Function::Unknown,
},
None,
),
];
for (i, (string, expected, expected_name)) in cases.into_iter().enumerate() {
let name: LedName = LedName::parse(string.into());
assert_eq!(name, expected, "Case {i} failed");
assert_eq!(name.parsed_name(), expected_name, "case {i} failed");
}
}
#[test]
fn initialize_dimmable() {
clean_up();
let name = "generic";
setup_test_env(&[name], 10, 100);
let led = Led::new(name.into()).expect("failed to initialize LED");
assert!(
matches!(led, LedType::Dimmable(_)),
"Initialized LED is not of dimmable type"
);
clean_up();
}
#[test]
fn initialize_non_dimmable() {
clean_up();
let name = "generic";
setup_test_env(&[name], 1, 1);
let led = Led::new(name.into()).expect("failed to initialize LED");
assert!(
matches!(led, LedType::NonDimmable(_)),
"Initialized LED is not of dimmable type"
);
clean_up();
}
#[test]
fn names() {
clean_up();
let names = ["led1", "led2", "led3"];
setup_test_env(&names, 0, 1);
let mut names_read: Vec<_> = led_names()
.expect("failed to get LED names")
.into_iter()
.map(|n| n.raw.into_owned())
.collect();
names_read.sort_unstable();
let names_read: Vec<&str> = names_read.iter().map(String::as_str).collect();
assert_eq!(
names.as_ref(),
&names_read,
"LED names read from the dir do not match"
);
clean_up();
}
#[test]
fn set_state() {
clean_up();
let name = "generic";
setup_test_env(&[name], 0, 1);
set_led_state(name, true).expect("failed to turn on LED");
let LedType::NonDimmable(mut led) = Led::new(name.into()).unwrap() else {
unreachable!()
};
assert_eq!(led.current(), 1, "LED is not turned on");
set_led_state(name, false).expect("failed to turn off LED");
led.reload();
assert_eq!(led.current(), 0, "LED is not turned off");
clean_up();
}
#[test]
fn get_state_on() {
clean_up();
let name = "generic";
setup_test_env(&[name], 1, 1);
assert!(
super::get_led_state(name).unwrap(),
"led state should return true, but returned false"
);
clean_up();
}
#[test]
fn get_state_off() {
clean_up();
let name = "generic";
setup_test_env(&[name], 0, 1);
assert!(
!super::get_led_state(name).unwrap(),
"led state should return false, but returned true"
);
clean_up();
}
#[test]
fn set_value() {
clean_up();
let name = "generic";
setup_test_env(&[name], 0, 255);
let LedType::Dimmable(mut led) = Led::new(name.into()).expect("failed to initialize LED")
else {
unreachable!()
};
let values = [0, 1, 2, 3, u8::MAX];
for val in values {
set_led_value(name, val).expect("failed to set led value");
led.reload();
assert_eq!(led.current(), val);
}
clean_up();
}
}