Skip to main content

input/
evdev.rs

1#![cfg(unix)]
2
3pub use evdev_upstream::*;
4
5use std::path::{Path, PathBuf};
6
7/// Crawls `/dev/input` for event-device paths without opening device nodes.
8///
9/// Opening remains the caller's responsibility so a compositor can discover
10/// nodes first and then use its restricted-open callback through logind.
11pub fn enumerate() -> EnumerateDevices {
12    enumerate_directory(Path::new("/dev/input"))
13}
14
15/// Event-device paths discovered without requiring read access to the nodes.
16pub struct EnumerateDevices {
17    paths: std::vec::IntoIter<PathBuf>,
18}
19
20impl Iterator for EnumerateDevices {
21    type Item = (PathBuf, ());
22
23    fn next(&mut self) -> Option<Self::Item> {
24        self.paths.next().map(|path| (path, ()))
25    }
26}
27
28fn enumerate_directory(directory: &Path) -> EnumerateDevices {
29    let mut paths = std::fs::read_dir(directory)
30        .into_iter()
31        .flatten()
32        .filter_map(Result::ok)
33        .map(|entry| entry.path())
34        .filter(|path| is_event_node(path))
35        .collect::<Vec<_>>();
36    paths.sort();
37    EnumerateDevices {
38        paths: paths.into_iter(),
39    }
40}
41
42fn is_event_node(path: &Path) -> bool {
43    let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
44        return false;
45    };
46    let Some(index) = name.strip_prefix("event") else {
47        return false;
48    };
49    !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit())
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use std::fs;
56    use std::os::unix::fs::symlink;
57    use std::time::{SystemTime, UNIX_EPOCH};
58
59    struct TestDirectory(PathBuf);
60
61    impl TestDirectory {
62        fn new() -> Self {
63            let nonce = SystemTime::now()
64                .duration_since(UNIX_EPOCH)
65                .expect("system clock precedes the Unix epoch")
66                .as_nanos();
67            let path = std::env::temp_dir().join(format!(
68                "libinput-rs-discovery-{}-{nonce}",
69                std::process::id()
70            ));
71            fs::create_dir(&path).expect("create test input directory");
72            Self(path)
73        }
74    }
75
76    impl Drop for TestDirectory {
77        fn drop(&mut self) {
78            let _ = fs::remove_dir_all(&self.0);
79        }
80    }
81
82    #[test]
83    fn enumeration_does_not_open_event_nodes() {
84        let directory = TestDirectory::new();
85        symlink(
86            directory.0.join("missing-target"),
87            directory.0.join("event0"),
88        )
89        .expect("create unopenable event node");
90        fs::write(directory.0.join("event12"), b"").expect("create event node");
91        fs::write(directory.0.join("eventx"), b"").expect("create invalid event name");
92        fs::write(directory.0.join("mouse0"), b"").expect("create non-event node");
93
94        let names = enumerate_directory(&directory.0)
95            .map(|(path, ())| {
96                path.file_name()
97                    .expect("event path has a file name")
98                    .to_string_lossy()
99                    .into_owned()
100            })
101            .collect::<Vec<_>>();
102
103        assert_eq!(names, vec!["event0".to_string(), "event12".to_string()]);
104    }
105
106    #[test]
107    fn missing_input_directory_is_empty() {
108        let directory = TestDirectory::new();
109        let missing = directory.0.join("missing");
110        assert_eq!(enumerate_directory(&missing).count(), 0);
111    }
112}