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}