rproj 0.13.1

Guided bootstrap-to-game-dev CLI for Roblox: takes a fresh Windows PC to a working Roblox/Luau setup, then scaffolds projects on it
mod model;
mod render;
#[cfg(test)]
mod tests;

use std::io::IsTerminal;

use anyhow::{Result, bail};
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};

use super::new;
use crate::{
    config::{GlobalConfig, Setups},
    tui,
};
use model::{Draft, Effect};

pub(super) use model::validate_name;

pub fn run(name: &str) -> Result<()> {
    if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
        bail!("Interactive creation requires a terminal; use `rproj new <name>`.");
    }
    model::validate_name(name).map_err(anyhow::Error::msg)?;
    let mut config = GlobalConfig::load()?;
    if !config.machine_configured() {
        bail!("Run Machine Setup from the hub, or `rproj setup`, before creating a project here.");
    }
    let (_, template) = new::prepare_project(&config, name)?;
    let root = config.projects_root()?;
    let mut draft = Draft::new(
        name,
        Setups::list(),
        config.selected_system_apps.clone(),
        config.selected_vscode_extensions.clone(),
    );
    let create = {
        let mut terminal = tui::TerminalSession::enter()?;
        loop {
            let mut small = false;
            terminal.draw(|frame| {
                small = tui::is_too_small(frame.area());
                render::draw(frame, &draft, &root.join(&draft.name).display().to_string());
            })?;
            let effect = match terminal.read_event()? {
                Event::Key(key) if key.kind != KeyEventKind::Release => {
                    if small
                        && !matches!(key.code, KeyCode::Esc | KeyCode::Char('?'))
                        && !(key.modifiers.contains(KeyModifiers::CONTROL)
                            && key.code == KeyCode::Char('c'))
                    {
                        continue;
                    }
                    draft.key(key)
                }
                Event::Paste(text) if !small => {
                    draft.paste(&text);
                    Effect::None
                }
                _ => Effect::None,
            };
            match effect {
                Effect::None => {}
                Effect::Cancel => break false,
                Effect::LoadSetup(name) => match new::read_setup(&name) {
                    Ok((graph, warnings)) => draft.loaded(&name, graph, warnings),
                    Err(error) => {
                        crate::diagnostics::event(
                            "creation.validation",
                            error
                                .to_string()
                                .lines()
                                .next()
                                .unwrap_or("Saved setup could not be loaded"),
                        );
                        draft.status = format!("{error:#}");
                    }
                },
                Effect::Create => {
                    if root.join(&draft.name).exists() {
                        crate::diagnostics::event(
                            "creation.validation",
                            "destination already exists",
                        );
                        draft.status =
                            "Destination already exists. Change the project name.".into();
                    } else if let Some(name) = draft
                        .save_setup
                        .as_deref()
                        .filter(|name| Setups::load(name).map_or(true, |setup| setup.is_some()))
                    {
                        crate::diagnostics::event(
                            "creation.validation",
                            "setup destination exists or is unreadable",
                        );
                        draft.status = format!(
                            "Setup `{name}` now exists or cannot be read. Choose another setup name."
                        );
                    } else {
                        break true;
                    }
                }
            }
        }
    };
    if !create {
        crate::diagnostics::event("creation.cancel", "nothing created");
        println!("Nothing created.");
        return Ok(());
    }
    crate::diagnostics::event(
        "creation.confirmed",
        format!(
            "workflow={:?}; capabilities={:?}; packages={:?}; dropped={:?}",
            draft.graph.package_workflow,
            draft.graph.capabilities,
            draft.graph.packages,
            draft.graph.dropped
        ),
    );
    let planned = draft.graph.plan(&draft.apps, &draft.extensions);
    new::execute_confirmed(
        &draft.name,
        &root.join(&draft.name),
        &draft.graph,
        &planned,
        template.as_ref(),
        &mut config,
        draft.save_setup.as_deref().map(new::SetupSave::New),
    )
}