xrandr_parser/lib.rs
1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright (c) 2022 Th3-S1lenc3
3
4//! # XRandR-Parser
5//!
6//! XRandR-Parser is a interface for parsing the output of `xrandr --query` into
7//! Rust Stuctures and filter through methods.
8//!
9//! ## Example
10//!
11//! Get the available resolutions for `HDMI-1` and the available refresh rates for `HDMI-1 @ 1920 x 1080`.
12//!
13//! ```edition2021
14//! #[allow(non_snake_case)]
15//!
16//! use xrandr_parser::Parser;
17//!
18//! fn main() -> Result<(), String> {
19//! let mut XrandrParser = Parser::new();
20//!
21//! XrandrParser.parse()?;
22//!
23//! let connector = &XrandrParser.get_connector("HDMI-1")?;
24//!
25//! let available_resolutions = &connector.available_resolutions_pretty()?;
26//! let available_refresh_rates = &connector.available_refresh_rates("1920x1080")?;
27//!
28//! println!(
29//! "Available Resolutions for HDMI-1: {:#?}",
30//! available_resolutions
31//! );
32//! println!(
33//! "Available Refresh Rates for HDMI-1 @ 1920x1080: {:#?}",
34//! available_refresh_rates
35//! );
36//! Ok(())
37//! }
38//! ```
39
40pub mod connector;
41
42use std::process::Command;
43use std::string::String;
44
45use crate::connector::*;
46
47#[derive(Default, Debug, serde::Serialize, serde::Deserialize)]
48pub struct Parser {
49 /// Every `Connector.name`
50 pub outputs: Vec<String>,
51
52 /// Every `Connector.name` where `Connector.status` is `true`
53 pub connected_outputs: Vec<String>,
54
55 /// Every Connector Struct
56 pub connectors: Vec<Connector>,
57
58 /// The Virtual Screen
59 pub screen: Screen,
60}
61
62#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
63pub struct Screen {
64 pub minimum: Resolution,
65 pub current: Resolution,
66 pub maximum: Resolution,
67}
68
69impl Parser {
70 /// Create a new instance of Parser
71 pub fn new() -> Parser {
72 Parser::default()
73 }
74
75 /// Populate properties of an instance of Parser from the output of `Parser::parse_query()`
76 pub fn parse(&mut self) -> Result<(), String> {
77 // Instatiate Properties
78 self.outputs = Vec::new();
79 self.connectors = Vec::new();
80 self.connected_outputs = Vec::new();
81
82 self.connectors = match Self::parse_query(self) {
83 Ok(r) => r,
84 Err(e) => return Err(e),
85 };
86
87 self.outputs = self.connectors.iter().map(|c| c.name.to_string()).collect();
88
89 self.connected_outputs = self
90 .connectors
91 .iter()
92 .filter(|c| c.status == "connected")
93 .map(|c| c.name.to_string())
94 .collect();
95
96 Ok(())
97 }
98
99 #[cfg(not(feature = "test"))]
100 fn exec_command() -> Result<String, String> {
101 let mut cmd = Command::new("sh");
102 cmd.arg("-c");
103
104 cmd.arg("xrandr --query | tr -s ' '");
105
106 let output = match cmd.output() {
107 Ok(r) => r,
108 Err(e) => return Err(e.to_string()),
109 };
110
111 if let Some(code) = output.status.code() {
112 if code != 0 {
113 let err_string = match String::from_utf8(output.stderr) {
114 Ok(r) => r,
115 Err(e) => return Err(e.to_string()),
116 };
117
118 return Err(err_string.to_string());
119 }
120 }
121
122 match String::from_utf8(output.stdout) {
123 Ok(r) => Ok(r),
124 Err(e) => Err(e.to_string()),
125 }
126 }
127
128 #[cfg(feature = "test")]
129 fn exec_command() -> Result<String, String> {
130 use std::env;
131
132 let mut cmd = Command::new("sh");
133 cmd.arg("-c");
134
135 let mut example_dir: String = "".to_string();
136
137 for (key, value) in env::vars() {
138 if key == "EXAMPLE_DIR" {
139 example_dir = value;
140 }
141 }
142
143 cmd.env("EXAMPLE_DIR", example_dir);
144 cmd.arg("cat $EXAMPLE_DIR/example_output");
145
146 let output = match cmd.output() {
147 Ok(r) => r,
148 Err(e) => return Err(e.to_string()),
149 };
150
151 if let Some(code) = output.status.code() {
152 if code != 0 {
153 let err_string = match String::from_utf8(output.stderr) {
154 Ok(r) => r,
155 Err(e) => return Err(e.to_string()),
156 };
157
158 return Err(err_string.to_string());
159 }
160 }
161
162 match String::from_utf8(output.stdout) {
163 Ok(r) => Ok(r),
164 Err(e) => return Err(e.to_string()),
165 }
166 }
167
168 /// Parse the output of `xrandr --query` and return it
169 fn parse_query(&mut self) -> Result<Vec<Connector>, String> {
170 let out_string = Self::exec_command()?;
171
172 let mut output: Vec<String> = out_string.split("\n").map(|s| s.to_string()).collect();
173 output.retain(|d| d != "");
174
175 let mut connectors: Vec<Connector> = Vec::new();
176 let mut active: Connector = Connector::default();
177
178 for o in &output {
179 let mut o_vec: Vec<&str> = o.split(" ").collect();
180 o_vec.retain(|s| s != &"");
181
182 if o_vec.contains(&"Screen") {
183 self.screen = Screen {
184 minimum: Resolution {
185 horizontal: o_vec[3].to_string(),
186 vertical: o_vec[5].replace(",", ""),
187 },
188 current: Resolution {
189 horizontal: o_vec[7].to_string(),
190 vertical: o_vec[9].replace(",", ""),
191 },
192 maximum: Resolution {
193 horizontal: o_vec[11].to_string(),
194 vertical: o_vec[13].replace(",", ""),
195 },
196 };
197
198 continue;
199 }
200
201 if o_vec.contains(&"connected") {
202 if active != Connector::default() {
203 connectors.push(active);
204 }
205 active = Connector::new();
206
207 active.set_name(o_vec[0].to_string());
208 active.set_status(o_vec[1].to_string());
209
210 let mut index = 2;
211
212 if o_vec[index] == "primary" {
213 active.set_primary(true);
214 } else {
215 active.set_primary(false);
216 index -= 1; // Shift 1 place left
217 }
218
219 index += 1;
220
221 let respos: Vec<&str> = o_vec[index].split(&['x', '+'][..]).collect();
222
223 active.set_current_resolution(Resolution {
224 horizontal: respos[0].to_string(),
225 vertical: respos[1].to_string(),
226 });
227
228 active.set_position(Position {
229 x: respos[2].to_string(),
230 y: respos[3].to_string(),
231 });
232
233 index += 1;
234
235 if o_vec[index].contains("(") {
236 active.set_orientation("normal".to_string());
237 } else {
238 active.set_orientation(o_vec[index].to_string());
239
240 index += 1;
241 }
242
243 let i7 = index + 7;
244
245 let filtered: Vec<String> =
246 o_vec[index..=i7].iter().map(|s| s.to_string()).collect();
247
248 let mut available_orientations: Vec<String> = Vec::new();
249 let mut i: usize = 0;
250
251 for ao in filtered {
252 let orientation: String = ao.replace(&['(', ')'][..], "");
253
254 if orientation == "axis" && i > 0 {
255 available_orientations[i - 1].push_str(" axis");
256 } else {
257 available_orientations.push(orientation);
258 i += 1;
259 }
260 }
261
262 active.set_available_orientations(available_orientations);
263
264 index += 8;
265
266 active.set_physical_dimensions(Dimensions {
267 x: o_vec[index].replace("mm", ""),
268 y: o_vec[index + 2].replace("mm", ""),
269 });
270
271 if o_vec.contains(&"disconnected") {
272 connectors.push(active);
273 active = Connector::default();
274 }
275
276 continue;
277 }
278
279 if o_vec.contains(&"disconnected") {
280 if active != Connector::default() {
281 connectors.push(active);
282 }
283 active = Connector::new();
284
285 active.set_name(o_vec[0].to_string());
286 active.set_status(o_vec[1].to_string());
287
288 continue;
289 }
290
291 if active != Connector::default() {
292 let mut outputs: Vec<Output> = active.output_info();
293
294 let mut rates: Vec<String> =
295 o_vec[1..].to_vec().iter().map(|s| s.to_string()).collect();
296
297 rates.retain(|r| r != "");
298
299 let resolution: Vec<&str> = o_vec[0].split('x').collect();
300
301 for r in &rates {
302 if r.contains('+') {
303 active.set_prefered_resolution(Resolution {
304 horizontal: resolution[0].to_string(),
305 vertical: resolution[1].to_string(),
306 });
307 active.set_prefered_refresh_rate(r.replace(&['+', '*'][..], ""));
308 }
309
310 if r.contains('*') {
311 active.set_current_refresh_rate(r.replace(&['+', '*'][..], ""));
312 }
313 }
314
315 rates = rates
316 .iter()
317 .map(|r| r.replace(&['+', '*'][..], ""))
318 .collect();
319
320 outputs.push(Output {
321 resolution: Resolution {
322 horizontal: resolution[0].to_string(),
323 vertical: resolution[1].to_string(),
324 },
325 rates,
326 });
327
328 active.set_output_info(outputs.to_vec());
329 }
330 }
331
332 if active != Connector::default() {
333 connectors.push(active);
334 }
335
336 Ok(connectors)
337 }
338
339 /// Getter function for `Parser.outputs`
340 ///
341 /// ## Example
342 ///
343 /// ```edition2021
344 /// #[allow(non_snake_case)]
345 ///
346 /// use xrandr_parser::Parser;
347 ///
348 /// fn main() -> Result<(), String> {
349 /// let mut XrandrParser = Parser::new();
350 ///
351 /// XrandrParser.parse()?;
352 ///
353 /// let outputs = &XrandrParser.outputs();
354 ///
355 /// # assert_eq!(outputs, &vec![
356 /// # "HDMI-1".to_string(),
357 /// # "HDMI-2".to_string(),
358 /// # ]);
359 /// Ok(())
360 /// }
361 /// ```
362 pub fn outputs(&self) -> Vec<String> {
363 self.outputs.to_vec()
364 }
365
366 /// Getter function for `Parser.connected_outputs`
367 ///
368 /// ## Example
369 ///
370 /// ```edition2021
371 /// #[allow(non_snake_case)]
372 ///
373 /// use xrandr_parser::Parser;
374 ///
375 /// fn main() -> Result<(), String> {
376 /// let mut XrandrParser = Parser::new();
377 ///
378 /// XrandrParser.parse()?;
379 ///
380 /// let connected_outputs = &XrandrParser.connected_outputs();
381 ///
382 /// println!("Connected Outputs: {:?}", connected_outputs);
383 ///
384 /// # assert_eq!(connected_outputs, &vec![
385 /// # "HDMI-1".to_string(),
386 /// # ]);
387 /// Ok(())
388 /// }
389 /// ```
390 pub fn connected_outputs(&self) -> Vec<String> {
391 self.connected_outputs.to_vec()
392 }
393
394 /// Getter function for `Parser.connectors`
395 ///
396 /// ## Example
397 ///
398 /// ```edition2021
399 /// #[allow(non_snake_case)]
400 ///
401 /// use xrandr_parser::Parser;
402 /// # use xrandr_parser::connector::*;
403 ///
404 /// fn main() -> Result<(), String> {
405 /// let mut XrandrParser = Parser::new();
406 ///
407 /// XrandrParser.parse()?;
408 ///
409 /// let connectors = &XrandrParser.connectors();
410 ///
411 /// println!("Connectors: {:#?}", connectors);
412 ///
413 /// # assert_eq!(connectors, &vec![
414 /// # Connector {
415 /// # name: "HDMI-1".to_string(),
416 /// # status: "connected".to_string(),
417 /// # primary: true,
418 /// # current_resolution: Resolution {
419 /// # horizontal: "1920".to_string(),
420 /// # vertical: "1080".to_string(),
421 /// # },
422 /// # current_refresh_rate: "60.00".to_string(),
423 /// # prefered_resolution: Resolution {
424 /// # horizontal: "1920".to_string(),
425 /// # vertical: "1080".to_string(),
426 /// # },
427 /// # prefered_refresh_rate: "60.00".to_string(),
428 /// # position: Position {
429 /// # x: "0".to_string(),
430 /// # y: "0".to_string(),
431 /// # },
432 /// # orientation: "normal".to_string(),
433 /// # available_orientations: [
434 /// # "normal".to_string(),
435 /// # "left".to_string(),
436 /// # "inverted".to_string(),
437 /// # "right".to_string(),
438 /// # "x axis".to_string(),
439 /// # "y axis".to_string(),
440 /// # ].to_vec(),
441 /// # physical_dimensions: Dimensions {
442 /// # x: "1210".to_string(),
443 /// # y: "680".to_string(),
444 /// # },
445 /// # output_info: [
446 /// # Output {
447 /// # resolution: Resolution {
448 /// # horizontal: "1920".to_string(),
449 /// # vertical: "1080".to_string(),
450 /// # },
451 /// # rates: [
452 /// # "60.00".to_string(),
453 /// # ].to_vec(),
454 /// # },
455 /// # ].to_vec(),
456 /// # },
457 /// # Connector {
458 /// # name: "HDMI-2".to_string(),
459 /// # status: "disconnected".to_string(),
460 /// # primary: false,
461 /// # current_resolution: Resolution {
462 /// # horizontal: "".to_string(),
463 /// # vertical: "".to_string(),
464 /// # },
465 /// # current_refresh_rate: "".to_string(),
466 /// # prefered_resolution: Resolution {
467 /// # horizontal: "".to_string(),
468 /// # vertical: "".to_string(),
469 /// # },
470 /// # prefered_refresh_rate: "".to_string(),
471 /// # position: Position {
472 /// # x: "".to_string(),
473 /// # y: "".to_string(),
474 /// # },
475 /// # orientation: "".to_string(),
476 /// # available_orientations: [].to_vec(),
477 /// # physical_dimensions: Dimensions {
478 /// # x: "".to_string(),
479 /// # y: "".to_string(),
480 /// # },
481 /// # output_info: [].to_vec(),
482 /// # },
483 /// # ]);
484 /// Ok(())
485 /// }
486 /// ```
487 pub fn connectors(&self) -> Vec<Connector> {
488 self.connectors.to_vec()
489 }
490
491 /// Getter function for `Parser.screen`
492 ///
493 /// ## Example
494 ///
495 /// ```edition2021
496 /// #[allow(non_snake_case)]
497 ///
498 /// use xrandr_parser::Parser;
499 /// # use xrandr_parser::connector::*;
500 /// # use xrandr_parser::Screen;
501 ///
502 /// fn main() -> Result<(), String> {
503 /// let mut XrandrParser = Parser::new();
504 ///
505 /// XrandrParser.parse()?;
506 ///
507 /// let screen = &XrandrParser.screen();
508 ///
509 /// println!("Screen Information: {:#?}", screen);
510 ///
511 /// # assert_eq!(screen, &Screen {
512 /// # minimum: Resolution {
513 /// # horizontal: "320".to_string(),
514 /// # vertical: "200".to_string(),
515 /// # },
516 /// # current: Resolution {
517 /// # horizontal: "1920".to_string(),
518 /// # vertical: "1080".to_string(),
519 /// # },
520 /// # maximum: Resolution {
521 /// # horizontal: "16384".to_string(),
522 /// # vertical: "16384".to_string(),
523 /// # },
524 /// # });
525 /// Ok(())
526 /// }
527 /// ```
528 pub fn screen(&self) -> Screen {
529 self.screen.clone()
530 }
531
532 /// Get the connector struct for a with the name provided. Returns `"Not found"` if the connector
533 /// is not found in `self.connectors`.
534 ///
535 /// ## Example
536 ///
537 /// ```edition2021
538 /// #[allow(non_snake_case)]
539 ///
540 /// use xrandr_parser::Parser;
541 /// # use xrandr_parser::connector::*;
542 ///
543 /// fn main() -> Result<(), String> {
544 /// let mut XrandrParser = Parser::new();
545 ///
546 /// XrandrParser.parse()?;
547 ///
548 /// let connector = &XrandrParser.get_connector("HDMI-1")?;
549 ///
550 /// println!("Connector - HDMI-1: {:#?}", connector);
551 ///
552 /// # assert_eq!(connector, &Connector {
553 /// # name: "HDMI-1".to_string(),
554 /// # status: "connected".to_string(),
555 /// # primary: true,
556 /// # current_resolution: Resolution {
557 /// # horizontal: "1920".to_string(),
558 /// # vertical: "1080".to_string(),
559 /// # },
560 /// # current_refresh_rate: "60.00".to_string(),
561 /// # prefered_resolution: Resolution {
562 /// # horizontal: "1920".to_string(),
563 /// # vertical: "1080".to_string(),
564 /// # },
565 /// # prefered_refresh_rate: "60.00".to_string(),
566 /// # position: Position {
567 /// # x: "0".to_string(),
568 /// # y: "0".to_string(),
569 /// # },
570 /// # orientation: "normal".to_string(),
571 /// # available_orientations: [
572 /// # "normal".to_string(),
573 /// # "left".to_string(),
574 /// # "inverted".to_string(),
575 /// # "right".to_string(),
576 /// # "x axis".to_string(),
577 /// # "y axis".to_string(),
578 /// # ]
579 /// # .to_vec(),
580 /// # physical_dimensions: Dimensions {
581 /// # x: "1210".to_string(),
582 /// # y: "680".to_string(),
583 /// # },
584 /// # output_info: [Output {
585 /// # resolution: Resolution {
586 /// # horizontal: "1920".to_string(),
587 /// # vertical: "1080".to_string(),
588 /// # },
589 /// # rates: ["60.00".to_string()].to_vec(),
590 /// # }]
591 /// # .to_vec(),
592 /// # });
593 /// Ok(())
594 /// }
595 /// ```
596 pub fn get_connector(&self, connector: &str) -> Result<Connector, String> {
597 for c in &self.connectors {
598 if c.name == connector {
599 return Ok(c.clone());
600 }
601 }
602
603 Err("Not Found".to_string())
604 }
605}