rpgmv_tool/command/
generate_completions.rs

1use crate::Options as RootOptions;
2use anyhow::Context;
3use anyhow::bail;
4use anyhow::ensure;
5use clap::CommandFactory;
6use clap::Parser;
7use clap_complete::Shell;
8use std::fs::File;
9use std::io::Read;
10use std::io::Write;
11use std::path::PathBuf;
12use std::process::Command;
13
14#[derive(Debug, Parser)]
15#[command(about = "Generate shell completions")]
16pub struct Options {
17    #[arg(short = 'o', long = "output")]
18    output: Option<PathBuf>,
19
20    #[arg(short = 's', long = "shell")]
21    shell: Option<Shell>,
22
23    #[arg(short = 'i', long = "install")]
24    install: bool,
25}
26
27fn install(shell: Shell, command: &mut clap::Command, command_name: &str) -> anyhow::Result<()> {
28    if !cfg!(windows) {
29        bail!("The --install flag is not supported on this platform.");
30    }
31
32    if shell != Shell::PowerShell {
33        bail!("The {shell} shell is not supported");
34    }
35
36    let output = Command::new("powershell.exe")
37        .arg("-NoProfile")
38        .args(["-Command", "Write-Host $PROFILE -NoNewline"])
39        .output()?;
40    ensure!(output.status.success());
41    let profile =
42        String::from_utf8(output.stdout).context("$PROFILE path contains invalid unicode")?;
43    let profile = PathBuf::from(profile);
44    let profile_parent = profile.parent().context("Missing $PROFILE parent folder")?;
45    let completions_dir = profile_parent.join("Completions");
46
47    match std::fs::create_dir(&completions_dir) {
48        Ok(()) => {}
49        Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
50        Err(error) => {
51            return Err(error).context("Failed to create Completions folder");
52        }
53    }
54
55    let completion_file_name = format!("{command_name}.ps1");
56    let completion_path = completions_dir.join(&completion_file_name);
57    let completion_path_temp = completion_path.with_added_extension("temp");
58    let mut file = File::options()
59        .create(true)
60        .write(true)
61        .read(false)
62        .truncate(false)
63        .open(&completion_path_temp)
64        .context("Failed to open completion file")?;
65    file.try_lock()
66        .context("failed to lock temp file for writing")?;
67    file.set_len(0)?;
68    clap_complete::generate(shell, command, command_name, &mut file);
69    file.flush()?;
70    file.sync_all()?;
71    drop(file);
72    std::fs::rename(completion_path_temp, completion_path)?;
73
74    let mut file = File::options()
75        .create(true)
76        .write(true)
77        .read(true)
78        .truncate(false)
79        .open(&profile)
80        .context("Failed to open $PROFILE")?;
81    file.try_lock()
82        .context("Failed to lock $PROFILE for writing")?;
83
84    let completion_line = format!(r". $PSScriptRoot\Completions\{completion_file_name}");
85    let mut profile_string = String::new();
86    file.read_to_string(&mut profile_string)?;
87    let has_completion_line = profile_string.lines().any(|line| line == completion_line);
88
89    if !has_completion_line {
90        if !profile_string.ends_with('\n') {
91            file.write_all(b"\n")?;
92        }
93        file.write_all(completion_line.as_bytes())?;
94    }
95    file.flush()?;
96    file.sync_all()?;
97    drop(file);
98
99    Ok(())
100}
101
102pub fn exec(options: Options) -> anyhow::Result<()> {
103    let mut command = RootOptions::command();
104
105    let shell = options
106        .shell
107        .map(Ok)
108        .unwrap_or_else(|| Shell::from_env().context("Failed to determine shell"))?;
109
110    let command_name = command.get_name().to_string();
111
112    let stdout = std::io::stdout();
113    let mut output: Box<dyn Write> = match options.output {
114        Some(output) => {
115            let file = File::create(&output)
116                .with_context(|| format!("Failed to create file \"{}\"", output.display()))?;
117            Box::new(file)
118        }
119        None => {
120            let lock = stdout.lock();
121            Box::new(lock)
122        }
123    };
124
125    clap_complete::generate(shell, &mut command, &command_name, &mut output);
126
127    if options.install {
128        install(shell, &mut command, &command_name)?;
129    }
130
131    Ok(())
132}