Skip to main content

cargo_airbender/commands/new/
args.rs

1use crate::cli::{NewAllocatorArg, NewArgs, NewProverBackendArg};
2use crate::error::{CliError, Result};
3use dialoguer::{Confirm, Input, Select};
4use std::path::{Component, Path, PathBuf};
5
6pub(super) struct ResolvedNewArgs {
7    pub(super) path: PathBuf,
8    pub(super) project_name: String,
9    pub(super) enable_std: bool,
10    pub(super) allocator: NewAllocatorArg,
11    pub(super) prover_backend: NewProverBackendArg,
12    pub(super) sdk_path: Option<PathBuf>,
13    pub(super) sdk_version: Option<String>,
14}
15
16pub(super) fn resolve_new_args(args: NewArgs) -> Result<ResolvedNewArgs> {
17    let NewArgs {
18        path,
19        name,
20        enable_std,
21        allocator,
22        prover_backend,
23        yes,
24        sdk_path,
25        sdk_version,
26    } = args;
27
28    let path = path.unwrap_or_else(|| PathBuf::from("."));
29    ensure_empty_dir(&path)?;
30
31    let inferred_name = infer_project_name(&path)?;
32    let default_name = name.or(inferred_name);
33
34    let (project_name, enable_std, allocator, prover_backend) = if yes {
35        let project_name = default_name.ok_or_else(|| {
36            CliError::new(format!(
37                "could not infer project name from destination `{}`",
38                path.display()
39            ))
40            .with_hint("pass an explicit project name with `--name <project-name>`")
41        })?;
42
43        (project_name, enable_std, allocator, prover_backend)
44    } else {
45        ensure_interactive_terminal()?;
46        (
47            prompt_project_name(default_name.as_deref())?,
48            prompt_enable_std(enable_std)?,
49            prompt_allocator(allocator)?,
50            prompt_prover_backend(prover_backend)?,
51        )
52    };
53
54    Ok(ResolvedNewArgs {
55        path,
56        project_name,
57        enable_std,
58        allocator,
59        prover_backend,
60        sdk_path,
61        sdk_version,
62    })
63}
64
65fn infer_project_name(path: &Path) -> Result<Option<String>> {
66    if let Some(name) = normalize_name(path.file_name()) {
67        return Ok(Some(name));
68    }
69
70    if is_current_dir_path(path) {
71        let current_dir = std::env::current_dir()
72            .map_err(|err| CliError::with_source("failed to determine current directory", err))?;
73        return Ok(normalize_name(current_dir.file_name()));
74    }
75
76    Ok(None)
77}
78
79fn is_current_dir_path(path: &Path) -> bool {
80    let mut components = path.components();
81    matches!(components.next(), Some(Component::CurDir))
82        && components.all(|component| component == Component::CurDir)
83}
84
85fn normalize_name(component: Option<&std::ffi::OsStr>) -> Option<String> {
86    component
87        .map(|value| value.to_string_lossy().trim().to_string())
88        .filter(|value| !value.is_empty() && value != "." && value != "..")
89}
90
91fn ensure_interactive_terminal() -> Result<()> {
92    use std::io::IsTerminal;
93
94    if std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
95        return Ok(());
96    }
97
98    Err(CliError::new("interactive mode requires a terminal")
99        .with_hint("pass `--yes` to run non-interactively and provide values via flags"))
100}
101
102fn prompt_project_name(default_name: Option<&str>) -> Result<String> {
103    let mut prompt = Input::<String>::new().with_prompt("Name of the project");
104    if let Some(default_name) = default_name {
105        prompt = prompt.with_initial_text(default_name.to_string());
106    }
107
108    let project_name = prompt
109        .validate_with(|input: &String| {
110            if input.trim().is_empty() {
111                Err("project name cannot be empty")
112            } else {
113                Ok(())
114            }
115        })
116        .interact_text()
117        .map_err(map_prompt_error)?;
118
119    Ok(project_name.trim().to_string())
120}
121
122fn prompt_enable_std(default: bool) -> Result<bool> {
123    Confirm::new()
124        .with_prompt("Enable STD")
125        .default(default)
126        .interact()
127        .map_err(map_prompt_error)
128}
129
130fn prompt_allocator(default: NewAllocatorArg) -> Result<NewAllocatorArg> {
131    let options = ["talc", "bump", "custom"];
132    let default_index = match default {
133        NewAllocatorArg::Talc => 0,
134        NewAllocatorArg::Bump => 1,
135        NewAllocatorArg::Custom => 2,
136    };
137
138    let selected = Select::new()
139        .with_prompt("Which allocator to use")
140        .items(&options)
141        .default(default_index)
142        .interact()
143        .map_err(map_prompt_error)?;
144
145    Ok(match selected {
146        0 => NewAllocatorArg::Talc,
147        1 => NewAllocatorArg::Bump,
148        2 => NewAllocatorArg::Custom,
149        _ => {
150            return Err(CliError::new(format!(
151                "invalid allocator selection index `{selected}`"
152            )));
153        }
154    })
155}
156
157fn prompt_prover_backend(default: NewProverBackendArg) -> Result<NewProverBackendArg> {
158    let options = [
159        "dev (mock proof for development)",
160        "gpu (real proving; CUDA required)",
161    ];
162    let default_index = match default {
163        NewProverBackendArg::Dev => 0,
164        NewProverBackendArg::Gpu => 1,
165    };
166
167    let selected = Select::new()
168        .with_prompt("Which prover backend to use")
169        .items(&options)
170        .default(default_index)
171        .interact()
172        .map_err(map_prompt_error)?;
173
174    Ok(match selected {
175        0 => NewProverBackendArg::Dev,
176        1 => NewProverBackendArg::Gpu,
177        _ => {
178            return Err(CliError::new(format!(
179                "invalid prover backend selection index `{selected}`"
180            )));
181        }
182    })
183}
184
185fn map_prompt_error(err: dialoguer::Error) -> CliError {
186    CliError::with_source("failed to read interactive input", err)
187        .with_hint("pass `--yes` to run non-interactively and provide values via flags")
188}
189
190fn ensure_empty_dir(path: &Path) -> Result<()> {
191    if path.exists()
192        && path
193            .read_dir()
194            .map_err(|err| {
195                CliError::with_source(
196                    format!("failed to list directory `{}`", path.display()),
197                    err,
198                )
199            })?
200            .next()
201            .is_some()
202    {
203        return Err(CliError::new(format!(
204            "destination directory `{}` is not empty",
205            path.display()
206        ))
207        .with_hint("choose a new path or remove existing files in that directory"));
208    }
209
210    Ok(())
211}