numlockctl_linux/
lib.rs

1//! Easy way to trigger and get state of numlock in linux
2
3use std::{fs, io, path::PathBuf, thread::sleep, time::Duration};
4
5/// Error type
6#[derive(thiserror::Error, Debug)]
7pub enum Error {
8    #[error("Unexpected IO error '{0}'")]
9    IoError(#[from] io::Error),
10    #[error("Failed to interpret LED state with path '{0}'")]
11    InvalidLedState(PathBuf),
12    #[error("Unexpected UInput Error '{0}'")]
13    UInputError(#[from] uinput::Error),
14    #[error("No valid LEDs found")]
15    NoLedsFound,
16    #[error("Failed to change numlock state")]
17    FailedToPressNumlock,
18}
19
20/// Result type alias
21pub type Res<T> = std::result::Result<T, Error>;
22
23/// State - ON/OFF
24#[derive(Debug, Copy, Clone, Eq, PartialEq)]
25pub enum State {
26    OFF,
27    ON,
28}
29
30impl State {
31    pub fn toggled(&self) -> State {
32        match self {
33            State::OFF => State::ON,
34            State::ON => State::OFF,
35        }
36    }
37}
38
39/// Gets the led state of a numlock led given its path
40pub fn get_led_state(path: &PathBuf) -> Res<State> {
41    let brightness_bytes = fs::read(path)?;
42    let Ok(state) = String::from_utf8(brightness_bytes.clone()) else {
43        return Err(Error::InvalidLedState(path.clone()));
44    };
45    if state == "0\n" {
46        Ok(State::OFF)
47    } else if state == "1\n" {
48        Ok(State::ON)
49    } else {
50        return Err(Error::InvalidLedState(path.clone()));
51    }
52}
53
54/// Gets a numlock led path and its state
55pub fn get_led_path_and_state() -> Res<(PathBuf, State)> {
56    const LEDS: &str = "/sys/class/leds/";
57    let entries = std::fs::read_dir(LEDS)?;
58    for entry in entries {
59        let entry = entry?;
60        let file_name_os_str = entry.file_name();
61        let file_name = file_name_os_str.to_string_lossy();
62        if !file_name.contains("numlock") {
63            continue;
64        }
65        let metadata = entry.metadata()?;
66        let metadata = if metadata.is_symlink() {
67            fs::metadata(entry.path())?
68        } else {
69            metadata
70        };
71        if !metadata.is_dir() {
72            continue;
73        }
74        let brightness_path = entry.path().join("brightness");
75        let state = get_led_state(&brightness_path)?;
76        return Ok((brightness_path, state));
77    }
78
79    Err(Error::NoLedsFound)
80}
81
82/// Emulates press of a numlock key. If `led_path_and_state` is provided, it is checked if the
83/// state got toggled.
84pub fn press_numlock(led_path_and_state: Option<(&PathBuf, State)>) -> Res<()> {
85    let device = uinput::default()?;
86    let device = device.name("numlocklrs")?;
87
88    let numlock_key = uinput::event::Keyboard::Key(uinput::event::keyboard::Key::NumLock);
89    let device = device.event(numlock_key)?;
90    let mut device = device.create()?;
91
92    sleep(Duration::from_micros(100000));
93
94    device.synchronize()?;
95    device.press(&numlock_key)?;
96    device.synchronize()?;
97    device.release(&numlock_key)?;
98    device.synchronize()?;
99
100    if let Some((path, state)) = led_path_and_state {
101        for _ in 0..10 {
102            sleep(Duration::from_micros(10000));
103            if get_led_state(&path)? != state {
104                return Ok(());
105            }
106        }
107        Err(Error::FailedToPressNumlock)
108    } else {
109        sleep(Duration::from_micros(100000));
110        Ok(())
111    }
112}