rpgmv_tool/command/
generate_completions.rs1use 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}