1use regex::Regex;
2use std::collections::HashMap;
3use std::fmt;
4use std::fs::File;
5use std::io::prelude::*;
6use std::path::{Path, PathBuf};
7use std::sync::OnceLock;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum HDMISource {
12 Cable,
13 Gpu,
14 Unknown,
15}
16
17impl fmt::Display for HDMISource {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 HDMISource::Cable => {
21 write!(f, "cable")
22 }
23 HDMISource::Gpu => {
24 write!(f, "gpu")
25 }
26 _ => {
27 write!(f, "unknown")
28 }
29 }
30 }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum HDMICableState {
36 Connected,
37 Unconnected,
38 Unknown,
39}
40
41impl fmt::Display for HDMICableState {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 HDMICableState::Connected => {
45 write!(f, "connected")
46 }
47 HDMICableState::Unconnected => {
48 write!(f, "unconnected")
49 }
50 _ => {
51 write!(f, "unknown")
52 }
53 }
54 }
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub enum Zone {
60 Head,
61 Left,
62 Right,
63}
64
65impl fmt::Display for Zone {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 Zone::Head => {
69 write!(f, "head")
70 }
71 Zone::Left => {
72 write!(f, "left")
73 }
74 Zone::Right => {
75 write!(f, "right")
76 }
77 }
78 }
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub struct HDMI {
84 pub source: HDMISource,
85 pub cable_state: HDMICableState,
86 pub exists: bool,
87}
88
89impl Default for HDMI {
90 fn default() -> Self {
91 Self {
92 source: HDMISource::Unknown,
93 cable_state: HDMICableState::Unknown,
94 exists: false,
95 }
96 }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub struct RGBZone {
102 pub zone: Zone,
103 pub red: u8,
104 pub green: u8,
105 pub blue: u8,
106}
107
108#[derive(Clone, Default, Debug, PartialEq, Eq)]
110pub struct RGBZones {
111 pub zones: HashMap<Zone, RGBZone>,
112 pub exists: bool,
113}
114
115pub struct Alienware {
117 platform: String,
118}
119
120impl Default for Alienware {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126impl Alienware {
127 pub fn new() -> Alienware {
129 Alienware {
130 platform: "/sys/devices/platform/alienware-wmi".to_string(),
131 }
132 }
133
134 #[allow(dead_code)]
136 fn test(platform: String) -> Alienware {
137 Alienware { platform }
138 }
139
140 pub fn is_alienware(&self) -> bool {
142 Path::new(&self.platform).exists()
143 }
144
145 pub fn get_hdmi(&self) -> std::io::Result<HDMI> {
147 let mut source = HDMISource::Unknown;
148 let mut cable_state = HDMICableState::Unknown;
149 let mut exists = false;
150 if self.is_alienware() {
151 exists = true;
152 let mut path_buf = PathBuf::new();
153 path_buf.push(&self.platform);
154 path_buf.push("hdmi");
155
156 if path_buf.exists() {
157 source = self.parse_source()?;
158 cable_state = self.parse_cable_state()?;
159 }
160 }
161 Ok(HDMI {
162 source,
163 cable_state,
164 exists,
165 })
166 }
167
168 fn parse_source(&self) -> std::io::Result<HDMISource> {
170 match self.parse_sys_file("hdmi/source") {
171 Ok(Some(s)) => {
172 if s.eq("cable") {
173 Ok(HDMISource::Cable)
174 } else if s.eq("gpu") {
175 Ok(HDMISource::Gpu)
176 } else {
177 Ok(HDMISource::Unknown)
178 }
179 }
180 Ok(None) => Ok(HDMISource::Unknown),
181 Err(x) => Err(x),
182 }
183 }
184
185 fn parse_cable_state(&self) -> std::io::Result<HDMICableState> {
187 match self.parse_sys_file("hdmi/cable") {
188 Ok(Some(s)) => {
189 if s.eq("connected") {
190 Ok(HDMICableState::Connected)
191 } else if s.eq("unconnected") {
192 Ok(HDMICableState::Unconnected)
193 } else {
194 Ok(HDMICableState::Unknown)
195 }
196 }
197 Ok(None) => Ok(HDMICableState::Unknown),
198 Err(x) => Err(x),
199 }
200 }
201
202 pub fn set_hdmi_source(self, source: HDMISource) -> std::io::Result<()> {
204 self.write_sys_file(
205 "hdmi/source",
206 match source {
207 HDMISource::Cable => "cable",
208 HDMISource::Gpu => "gpu",
209 HDMISource::Unknown => "unknown",
210 },
211 )?;
212 Ok(())
213 }
214
215 pub fn get_rgb_zones(&self) -> std::io::Result<RGBZones> {
217 let mut zones = HashMap::new();
218 let mut exists = false;
219 if self.is_alienware() {
220 exists = true;
221 let mut path_buf = PathBuf::new();
222 path_buf.push(&self.platform);
223 path_buf.push("rgb_zones");
224 if path_buf.exists() {
225 path_buf.push("zone00");
226 if path_buf.exists() {
227 zones.insert(
228 Zone::Head,
229 self.parse_rgb_zone(Zone::Head, "rgb_zones/zone00")?,
230 );
231 }
232
233 path_buf.pop();
234 path_buf.push("zone01");
235 if path_buf.exists() {
236 zones.insert(
237 Zone::Left,
238 self.parse_rgb_zone(Zone::Left, "rgb_zones/zone01")?,
239 );
240 }
241
242 path_buf.pop();
243 path_buf.push("zone02");
244 if path_buf.exists() {
245 zones.insert(
246 Zone::Right,
247 self.parse_rgb_zone(Zone::Right, "rgb_zones/zone02")?,
248 );
249 }
250 }
251 }
252 Ok(RGBZones { zones, exists })
253 }
254
255 pub fn set_rgb_zone(&self, zone: Zone, red: u8, green: u8, blue: u8) -> std::io::Result<()> {
257 let rgb = format!("{red:02x}{green:02x}{blue:02x}");
258 self.write_sys_file(
259 match zone {
260 Zone::Head => "rgb_zones/zone00",
261 Zone::Left => "rgb_zones/zone01",
262 Zone::Right => "rgb_zones/zone02",
263 },
264 rgb.as_str(),
265 )?;
266 Ok(())
267 }
268
269 fn parse_rgb_zone(&self, zone: Zone, file_name: &str) -> std::io::Result<RGBZone> {
271 match self.parse_sys_rgb_file(file_name) {
272 Ok((red, green, blue)) => Ok(RGBZone {
273 zone,
274 red,
275 green,
276 blue,
277 }),
278 Err(x) => Err(x),
279 }
280 }
281
282 pub fn has_hdmi(self) -> bool {
284 if let Ok(hdmi) = self.get_hdmi() {
285 hdmi.exists
286 } else {
287 false
288 }
289 }
290
291 pub fn has_rgb_zones(self) -> bool {
293 if let Ok(rgb_zones) = self.get_rgb_zones() {
294 rgb_zones.exists
295 } else {
296 false
297 }
298 }
299
300 fn parse_sys_file(&self, file_name: &str) -> std::io::Result<Option<String>> {
302 static RE: OnceLock<Regex> = OnceLock::new();
303 let re = RE.get_or_init(|| Regex::new(r"\[([^)]+)\]").unwrap());
304 let mut path_buf = PathBuf::new();
305 path_buf.push(&self.platform);
306 path_buf.push(file_name);
307 let mut file = File::open(path_buf.as_path())?;
308 let mut contents = String::new();
309 file.read_to_string(&mut contents).unwrap();
310 let caps = re.captures(contents.as_str()).unwrap();
311 match caps.len() > 0 {
312 true => Ok(Some(caps[1].to_string())),
313 false => Ok(None),
314 }
315 }
316
317 fn parse_sys_rgb_file(&self, file_name: &str) -> std::io::Result<(u8, u8, u8)> {
319 static RE: OnceLock<Regex> = OnceLock::new();
320 let re = RE.get_or_init(|| Regex::new(r"^red: (\d+), green: (\d+), blue: (\d+)").unwrap());
321 let mut path_buf = PathBuf::new();
322 path_buf.push(&self.platform);
323 path_buf.push(file_name);
324 let mut file = File::open(path_buf)?;
325 let mut contents = String::new();
326 file.read_to_string(&mut contents).unwrap();
327 match re.captures(contents.as_str()) {
328 Some(caps) if caps.len() == 4 => {
329 let red = &caps[1];
330 let green = &caps[2];
331 let blue = &caps[3];
332 Ok((
333 red.parse::<u8>().unwrap(),
334 green.parse::<u8>().unwrap(),
335 blue.parse::<u8>().unwrap(),
336 ))
337 }
338 _ => Ok((0u8, 0u8, 0u8)),
339 }
340 }
341
342 fn write_sys_file(&self, file_name: &str, value: &str) -> std::io::Result<()> {
344 let mut path_buf = PathBuf::new();
345 path_buf.push(&self.platform);
346 path_buf.push(file_name);
347 let mut sys_file = File::create(path_buf)?;
348 sys_file.write_all(value.as_bytes())?;
349 Ok(())
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use crate::{HDMISource, Zone};
356 use std::fs::{create_dir_all, metadata, remove_dir_all, File};
357 use std::io::prelude::*;
358 use std::path::{Path, PathBuf};
359
360 #[test]
361 fn is_alienware() {
362 let alienware = crate::Alienware::test(setup_aw("is_alienware"));
363 let rtn = alienware.is_alienware();
364 assert!(rtn);
365 }
366
367 #[test]
368 fn is_not_alienware() {
369 let alienware = crate::Alienware::test(setup_not_aw("is_not_alienware"));
370 let rtn = alienware.is_alienware();
371 assert!(!rtn);
372 }
373
374 #[test]
375 fn has_rgb_zones() {
376 let alienware = crate::Alienware::test(setup_aw("has_rgb_zones"));
377 let rtn = alienware.has_rgb_zones();
378 assert!(rtn);
379 }
380
381 #[test]
382 fn get_rgb_zones() {
383 let alienware = crate::Alienware::test(setup_aw("get_rgb_zones"));
384 let rgbzone = alienware.get_rgb_zones();
385 assert!(rgbzone.is_ok());
386 if let Ok(rgbzone) = rgbzone {
387 assert_eq!(rgbzone.zones.len(), 3);
388 let head = rgbzone.zones.get(&crate::Zone::Head).unwrap();
389 assert_eq!(head.zone, crate::Zone::Head);
390 assert_eq!(head.red, 0u8);
391 assert_eq!(head.green, 0u8);
392 assert_eq!(head.blue, 15u8);
393 let left = rgbzone.zones.get(&crate::Zone::Left).unwrap();
394 assert_eq!(left.zone, crate::Zone::Left);
395 assert_eq!(left.red, 0u8);
396 assert_eq!(left.green, 15u8);
397 assert_eq!(left.blue, 0u8);
398 let right = rgbzone.zones.get(&crate::Zone::Right).unwrap();
399 assert_eq!(right.zone, crate::Zone::Right);
400 assert_eq!(right.red, 15u8);
401 assert_eq!(right.green, 0u8);
402 assert_eq!(right.blue, 0u8);
403 }
404 }
405
406 #[test]
407 fn set_rgb_zones() {
408 let alienware = crate::Alienware::test(setup_aw("set_rgb_zones"));
409 match alienware.set_rgb_zone(Zone::Left, 15, 7, 0) {
410 Err(_) => {
411 panic!("Failed to set the RGB Zone");
412 }
413 Ok(()) => {
414 let path = Path::new(
415 "/tmp/alienware_wmi_test/set_rgb_zones/alienware-wmi/rgb_zones/zone01",
416 );
417 assert!(path.exists());
418 let mut file = File::open(path).unwrap();
419 let mut contents = String::new();
420 file.read_to_string(&mut contents).unwrap();
421 assert_eq!("0f0700", contents);
422 }
423 }
424 }
425
426 #[test]
427 fn has_hdmi() {
428 let alienware = crate::Alienware::test(setup_aw("has_hdmi"));
429 let rtn = alienware.has_hdmi();
430 assert!(rtn);
431 }
432
433 #[test]
434 fn get_hdmi() {
435 let alienware = crate::Alienware::test(setup_aw("get_hdmi"));
436 let hdmi = alienware.get_hdmi();
437 assert!(hdmi.is_ok());
438 if let Ok(hdmi) = hdmi {
439 assert!(hdmi.exists);
440 assert_eq!(hdmi.source, crate::HDMISource::Gpu);
441 assert_eq!(hdmi.cable_state, crate::HDMICableState::Connected);
442 }
443 }
444
445 #[test]
446 fn set_hdmi_source() {
447 let alienware = crate::Alienware::test(setup_aw("set_hdmi_source"));
448 match alienware.set_hdmi_source(HDMISource::Cable) {
449 Err(_) => {
450 panic!("Failed to set the HDMI Source");
451 }
452 Ok(()) => {
453 let path = "/tmp/alienware_wmi_test/set_hdmi_source/hdmi/source";
454 if metadata(path).is_ok() {
455 let mut file = File::open(path).unwrap();
456 let mut contents = String::new();
457 file.read_to_string(&mut contents).unwrap();
458 assert_eq!("cable", contents);
459 }
460 }
461 }
462 }
463
464 const TEST_PATH: &str = "/tmp/alienware_wmi_test";
465
466 fn setup_not_aw(test: &str) -> String {
467 let mut path_buf = PathBuf::new();
468 path_buf.push(TEST_PATH);
469 path_buf.push(test);
470 if path_buf.exists() && remove_dir_all(path_buf.as_path()).is_err() {
471 panic!("Failed to remove test path while setting up not_aw scenario")
472 }
473 if create_dir_all(path_buf.as_path()).is_err() {
474 panic!("Failed to setup test path while setting up not_aw scenario")
475 };
476
477 path_buf.push("alienware-wmi");
478 let platform = path_buf.as_os_str().to_str().unwrap().to_string();
479 platform
480 }
481
482 fn setup_aw(test: &str) -> String {
483 let mut path_buf = PathBuf::new();
484 path_buf.push(TEST_PATH);
485 path_buf.push(test);
486 if metadata(path_buf.as_path()).is_ok() && remove_dir_all(path_buf.as_path()).is_err() {
487 panic!("Failed to remove test path while setting up aw scenario")
488 }
489 path_buf.push("alienware-wmi");
490 if create_dir_all(path_buf.as_path()).is_err() {
491 panic!("Failed to setup test path while setting up aw scenario")
492 };
493 path_buf.push("hdmi");
495 if create_dir_all(path_buf.as_path()).is_err() {
496 panic!("Failed to setup hdmi while setting up aw scenario")
497 };
498
499 path_buf.push("cable");
501 let mut file = File::create(path_buf.as_path()).unwrap();
502 file.write_all(b"unconnected [connected] unknown").unwrap();
503 path_buf.pop();
504
505 path_buf.push("source");
507 let mut file = File::create(path_buf.as_path()).unwrap();
508 file.write_all(b"cable [gpu] unknown,").unwrap();
509 path_buf.pop();
510
511 path_buf.pop();
512 path_buf.push("rgb_zones");
514 if create_dir_all(path_buf.as_path()).is_err() {
515 panic!("Failed to setup rgb_zones while setting up aw scenario")
516 };
517
518 path_buf.push("zone00");
520 let mut file = File::create(path_buf.as_path()).unwrap();
521 file.write_all(b"red: 0, green: 0, blue: 15").unwrap();
522 path_buf.pop();
523
524 path_buf.push("zone01");
526 let mut file = File::create(path_buf.as_path()).unwrap();
527 file.write_all(b"red: 0, green: 15, blue: 0").unwrap();
528 path_buf.pop();
529
530 path_buf.push("zone02");
532 let mut file = File::create(path_buf.as_path()).unwrap();
533 file.write_all(b"red: 15, green: 0, blue: 0").unwrap();
534 path_buf.pop();
535
536 path_buf.pop();
537
538 let platform = path_buf.as_os_str().to_str().unwrap().to_string();
539 platform
540 }
541}