cu29_rendercfg/
rendercfg.rs

1mod config;
2use clap::Parser;
3use config::{ConfigGraphs, read_configuration};
4pub use cu29_traits::*;
5use std::io::Cursor;
6use std::io::Write;
7use std::path::PathBuf;
8use std::process::{Command, Stdio};
9use tempfile::Builder;
10
11#[derive(Parser)]
12#[clap(author, version, about, long_about = None)]
13struct Args {
14    /// Config file name
15    #[clap(value_parser)]
16    config: PathBuf,
17    /// Mission id to render (omit to render every mission)
18    #[clap(long)]
19    mission: Option<String>,
20    /// List missions contained in the configuration and exit
21    #[clap(long, action)]
22    list_missions: bool,
23    /// Open the SVG in the default system viewer
24    #[clap(long)]
25    open: bool,
26}
27
28/// Render the configuration file to a dot file then convert it to an SVG and optionally opens it with inkscape.
29fn main() -> std::io::Result<()> {
30    // Parse command line arguments
31    let args = Args::parse();
32
33    let config = read_configuration(args.config.to_str().unwrap())
34        .expect("Failed to read configuration file");
35
36    if args.list_missions {
37        print_mission_list(&config);
38        return Ok(());
39    }
40
41    let mission = match validate_mission_arg(&config, args.mission.as_deref()) {
42        Ok(mission) => mission,
43        Err(err) => {
44            eprintln!("{err}");
45            std::process::exit(1);
46        }
47    };
48
49    let mut content = Vec::<u8>::new();
50    {
51        let mut cursor = Cursor::new(&mut content);
52        config.render(&mut cursor, mission.as_deref()).unwrap();
53    }
54
55    // Generate SVG from DOT
56    let mut child = Command::new("dot")
57        .arg("-Tsvg")
58        .stdin(Stdio::piped())
59        .stdout(Stdio::piped())
60        .spawn()
61        .expect("Failed to start dot process");
62
63    {
64        let stdin = child.stdin.as_mut().expect("Failed to open stdin");
65        let result = stdin.write_all(&content);
66        if let Err(e) = result {
67            eprintln!("Failed to write to stdin of the dot process: {e}");
68            std::process::exit(1);
69        }
70    }
71
72    let output = child.wait_with_output().expect("Failed to read stdout");
73
74    if !output.status.success() {
75        std::process::exit(1);
76    }
77
78    let graph_svg = output.stdout;
79    if args.open {
80        // Create a temporary file to store the SVG
81        let mut temp_file = Builder::new().suffix(".svg").tempfile()?;
82        temp_file.write_all(graph_svg.as_slice())?;
83
84        // Open the SVG in the default system viewer
85        Command::new("inkscape") // xdg-open fails silently (while it works from a standard bash on the same file :shrug:)
86            .arg(temp_file.path())
87            .status()
88            .expect("failed to open SVG file");
89    } else {
90        // Write the SVG content to a file
91        let mut svg_file = std::fs::File::create("output.svg")?;
92        svg_file.write_all(graph_svg.as_slice())?;
93    }
94    Ok(())
95}
96
97fn validate_mission_arg(
98    config: &config::CuConfig,
99    requested: Option<&str>,
100) -> CuResult<Option<String>> {
101    match (&config.graphs, requested) {
102        (ConfigGraphs::Simple(_), None) => Ok(None),
103        (ConfigGraphs::Simple(_), Some("default")) => Ok(None),
104        (ConfigGraphs::Simple(_), Some(id)) => Err(CuError::from(format!(
105            "Config is not mission-based; remove --mission (received '{id}')"
106        ))),
107        (ConfigGraphs::Missions(graphs), Some(id)) => {
108            if graphs.contains_key(id) {
109                Ok(Some(id.to_string()))
110            } else {
111                Err(CuError::from(format!(
112                    "Mission '{id}' not found. Available missions: {}",
113                    format_mission_list(graphs)
114                )))
115            }
116        }
117        (ConfigGraphs::Missions(_), None) => Ok(None),
118    }
119}
120
121fn print_mission_list(config: &config::CuConfig) {
122    match &config.graphs {
123        ConfigGraphs::Simple(_) => println!("default"),
124        ConfigGraphs::Missions(graphs) => {
125            let mut missions: Vec<_> = graphs.keys().cloned().collect();
126            missions.sort();
127            for mission in missions {
128                println!("{mission}");
129            }
130        }
131    }
132}
133
134fn format_mission_list(graphs: &hashbrown::HashMap<String, config::CuGraph>) -> String {
135    let mut missions: Vec<_> = graphs.keys().cloned().collect();
136    missions.sort();
137    missions.join(", ")
138}