Skip to main content

oseda_cli/cmd/
init.rs

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