Skip to main content

oseda_cli/cmd/
init.rs

1/*
2npm init -y
3npm install --save-dev vite http-server
4npm install reveal.js serve vite-plugin-singlefile
5touch vite.config.js -> add the plugin, write this by hand
6
7*/
8
9use std::{
10    error::Error,
11    fs::{self},
12    process::Command,
13    str::FromStr,
14};
15
16use clap::Args;
17use strum::IntoEnumIterator;
18
19use crate::{config, template::Template};
20
21/// Options for the `oseda init` command
22#[derive(Args, Debug)]
23pub struct InitOptions {
24    #[arg(long)]
25    pub title: Option<String>,
26
27    #[arg(long, num_args = 1.., value_delimiter=' ')]
28    pub tags: Option<Vec<String>>,
29
30    #[arg(long)]
31    pub color: Option<String>,
32
33    #[arg(long)]
34    pub template: Option<String>,
35}
36
37// embed all the static markdown template files into binary
38const MD_VITE_CONFIG_JS: &str = include_str!("../static/md-templates/vite.config.js");
39const MD_INDEX_HTML: &str = include_str!("../static/md-templates/index.html");
40const MD_MAIN_JS: &str = include_str!("../static/md-templates/main.js");
41const MD_SLIDES: &str = include_str!("../static/md-templates/slides.md");
42const MD_CUSTOM_CSS: &str = include_str!("../static/md-templates/custom.css");
43const MD_FERRIS: &[u8] = include_bytes!("../static/md-templates/ferris.png");
44
45// do the same with the html templates
46const HTML_VITE_CONFIG_JS: &str = include_str!("../static/html-templates/vite.config.js");
47const HTML_INDEX_HTML: &str = include_str!("../static/html-templates/index.html");
48const HTML_MAIN_JS: &str = include_str!("../static/html-templates/main.js");
49const HTML_SLIDES: &str = include_str!("../static/html-templates/slides.html");
50const HTML_CUSTOM_CSS: &str = include_str!("../static/html-templates/custom.css");
51const HTML_FERRIS: &[u8] = include_bytes!("../static/html-templates/ferris.png");
52
53/// Initialize an Oseda project with the provided options
54///
55/// This command will:
56/// - Run `npm init`
57/// - Install required dependencies (Vite, Reveal.js, etc)
58/// - Write config and boilerplate files
59///
60/// # Arguments
61/// * `_opts` - command-line options (this is unused rn, used later I hope)
62///
63/// # Returns
64/// * `Ok(())` if project initialization is suceeded
65/// * `Err` if any step (npm, file write, config generation etc) fails
66pub fn init(opts: InitOptions) -> Result<(), Box<dyn Error>> {
67    let template = match opts.template {
68        Some(ref arg_template) => {
69            Template::from_str(arg_template).map_err(|_| "Invalid template".to_string())?
70        }
71        None => prompt_template()?,
72    };
73
74    let conf = config::create_conf(opts)?;
75
76    std::fs::create_dir_all(&conf.title)?;
77
78    let output = Command::new("npm")
79        .args(["init", "-y", "--prefix", &conf.title])
80        .current_dir(&conf.title)
81        .output()?;
82
83    // swapped to explicit check so it doesn't hang after
84    if !output.status.success() {
85        eprintln!(
86            "npm init failed: {}",
87            String::from_utf8_lossy(&output.stderr)
88        );
89        return Err("npm init failed".into());
90    }
91
92    let npm_commands = vec![
93        format!("install --save-dev vite http-server"),
94        format!("install reveal.js serve vite-plugin-singlefile"),
95        format!("install vite@5"),
96        format!("install patch-package"),
97    ];
98
99    for c in npm_commands {
100        let args: Vec<&str> = c.split(' ').collect();
101        let output = Command::new("npm")
102            .args(&args)
103            .current_dir(&conf.title)
104            .output()?;
105
106        if !output.status.success() {
107            eprintln!(
108                "npm {} failed: {}",
109                c,
110                String::from_utf8_lossy(&output.stderr)
111            );
112            return Err(format!("npm {} failed", c).into());
113        }
114        println!("Bootstrapped npm {}", c);
115    }
116
117    println!("Saving config file...");
118
119    config::write_config(&conf.title, &conf)?;
120
121    // 99% sure we'll only ever have to maintain these two template schemas
122    match template {
123        Template::Markdown => {
124            // fs::write(format!("{}/package.json", &conf.title), MD_PACKAGE_JSON)?;
125            fs::write(format!("{}/vite.config.js", &conf.title), MD_VITE_CONFIG_JS)?;
126            fs::write(format!("{}/index.html", &conf.title), MD_INDEX_HTML)?;
127
128            std::fs::create_dir_all(format!("{}/src", &conf.title))?;
129            fs::write(format!("{}/src/main.js", &conf.title), MD_MAIN_JS)?;
130
131            std::fs::create_dir_all(format!("{}/slides", &conf.title))?;
132            fs::write(format!("{}/slides/slides.md", &conf.title), MD_SLIDES)?;
133
134            std::fs::create_dir_all(format!("{}/css", &conf.title))?;
135            fs::write(format!("{}/css/custom.css", &conf.title), MD_CUSTOM_CSS)?;
136
137            std::fs::create_dir_all(format!("{}/public", &conf.title))?;
138            fs::write(format!("{}/public/ferris.png", &conf.title), MD_FERRIS)?;
139        }
140        Template::HTML => {
141            // fs::write(format!("{}/package.json", &conf.title), HTML_PACKAGE_JSON)?;
142            fs::write(
143                format!("{}/vite.config.js", &conf.title),
144                HTML_VITE_CONFIG_JS,
145            )?;
146            fs::write(format!("{}/index.html", &conf.title), HTML_INDEX_HTML)?;
147
148            std::fs::create_dir_all(format!("{}/src", &conf.title))?;
149            fs::write(format!("{}/src/main.js", &conf.title), HTML_MAIN_JS)?;
150
151            std::fs::create_dir_all(format!("{}/slides", &conf.title))?;
152            fs::write(format!("{}/slides/slides.html", &conf.title), HTML_SLIDES)?;
153
154            std::fs::create_dir_all(format!("{}/css", &conf.title))?;
155            fs::write(format!("{}/css/custom.css", &conf.title), HTML_CUSTOM_CSS)?;
156
157            std::fs::create_dir_all(format!("{}/public", &conf.title))?;
158            fs::write(format!("{}/public/ferris.png", &conf.title), HTML_FERRIS)?
159        }
160    }
161
162    Ok(())
163}
164
165fn prompt_template() -> Result<Template, Box<dyn Error>> {
166    let template_opts: Vec<Template> = Template::iter().collect();
167
168    let chosen_template = inquire::Select::new("Select a template:", template_opts).prompt()?;
169
170    Ok(chosen_template)
171}