diff --git a/.gitignore b/.gitignore index 6985cf1..936b60c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,12 @@ debug/ target/ +# The name I usually give to my testing project +temp/ + # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb diff --git a/Cargo.toml b/Cargo.toml index d1ed7ae..0f67a3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ [package] -name = "project_initializer" +name = "pi" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = { version = "4.5.1", features = ["derive"] } colored = "2.1.0" diff --git a/README.md b/README.md index 8904ffa..8f480df 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,52 @@ -# pi -A CLI project initializer in Rust +# pi + +A CLI project initializer written in Rust. + +## Table of Contents + +* [Installation](#installation) +* [Usage](#usage) +* [Contributing](#contributing) +* [License](#license) +* [Additional Information](#additional-information) + +## Installation + +[Install Rust](https://www.rust-lang.org/tools/install) if you haven't, then clone this repo and build using the following commands: + +```bash +git clone https://github.com/uptudev/pi +cd ./pi +cargo build --release +``` + +This will build your executable in the `./target/release/` folder, with the filename `pi` on Unix-like systems, and `pi.exe` on Windows. Simply move this file to wherever your OS pulls binaries from, and you can build projects with the `pi` command. + +## Usage + +The base initializer is run simply via `pi`, but if the project name and language are given, this can streamline the initialization process. + +**Tips:** + +* Break down instructions into logical steps. +* Use bullet points for succinct explanations. +* Consider creating a separate "Getting Started" guide for beginners. + +## Contributing + +**Outline your contribution guidelines.** Explain how users can contribute to your project, whether through code, bug reports, or documentation improvements. Specify preferred code style, pull request format, and testing procedures. + +## License + +**Specify the license under which your project is distributed.** Use clear and concise language, and link to the full license text in the `LICENSE` file. + +## Additional Information + +**Include any other relevant information you want to share.** This could be links to related projects, documentation, support channels, or your contact information. + +**Remember:** + +* Keep your README.md file concise and focused. +* Use clear headings, formatting, and visuals for readability. +* Update your README.md file regularly to reflect changes in your project. + diff --git a/notes b/notes new file mode 100644 index 0000000..2dccf5f --- /dev/null +++ b/notes @@ -0,0 +1,16 @@ +IDEAS: + +migrate to a modular architecture with language functions being in their own folders for readability (libs.rs is getting a bit bloated) + +example file structure: +./src/ +|-> main.rs +|-> libs/ + |-> base.rs // contains query, routing, etc + |-> rust.rs + |-> js.rs + |-> ts.rs + |-> react.rs + |-> vue.rs + |-> svelte.rs + |-> mod.rs // module declaration diff --git a/src/libs.rs b/src/libs.rs deleted file mode 100644 index f4d4a85..0000000 --- a/src/libs.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::io::Write; - -pub fn query(prompt: &String, buffer: &mut String) { - print!("{}", prompt); - std::io::stdout().flush().expect("Error flushing buffer"); - std::io::stdin() - .read_line(buffer) - .expect("Failed to read input to buffer"); -} diff --git a/src/libs/base.rs b/src/libs/base.rs new file mode 100644 index 0000000..d0572c5 --- /dev/null +++ b/src/libs/base.rs @@ -0,0 +1,208 @@ +use std::io::Write; +use colored::*; +use std::{io::{stdout, Result}, process::Command, fs}; +use crate::libs::{ + js, react, rust, svelte, ts, vue, zig +}; + +pub fn start(name: Option, lang: Option) { + let mag_colour_code = get_trailing_char(); + let _ = + Command::new("clear") + .status(); + let mut buffer = String::new(); + title(); + check_directory(&mut buffer, &mag_colour_code); + + // option destruction syntax is hot garbage ngl + let proj_name = if let Some(n) = name { + n + } else { + get_project_name(&mut buffer, &mag_colour_code) + }; + let lang = if let Some(l) = lang { + l + } else { + get_language(&mut buffer, &mag_colour_code) + }; + route_response(&mut buffer, lang, &mag_colour_code, &proj_name); + stdout() + .flush() + .expect("Error flushing stdout."); +} + +/// prints the title +pub fn title() { + println!("{}", "┍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┑".black().dimmed()); + println!( + "{} {} {} {} {}", + "│".black().dimmed(), + "Project Initializer".bold().underline(), + "by".normal(), + "uptu".bold().cyan(), + "│".black().dimmed()); + println!("{}", "┕━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙".black().dimmed()); + println!("{}", "Please ensure you are in the parent directory of your intended project location before proceeding.".bold().red()); + let cwd = std::env::current_dir() + .expect("Error getting CWD") + .as_path() + .as_os_str() + .to_str() + .unwrap() + .to_owned(); + println!("{} {}","CWD:".purple().dimmed(), cwd.purple()); +} + +/// queries the user with a prompt, returning an owned String +pub fn query(prompt: &String, buffer: &mut String, mag: &str) -> String { + print!("{}{}", prompt, mag); + std::io::stdout().flush().expect("Error flushing buffer"); + std::io::stdin() + .read_line(buffer) + .expect("Failed to read input to buffer"); + buffer.trim().to_owned() +} + +/// Gets system magenta colour code via tput +pub fn get_trailing_char() -> String { + let mut magenta_res = std::process::Command::new("tput"); + magenta_res.args(["setaf", "5"]); + let res = magenta_res.output().unwrap(); + String::from_utf8(res.stdout).unwrap() +} + +/// queries the user to proceed after listing CWD +pub fn check_directory(buffer: &mut String, mag: &str) { + let prompt = format!( + "{} ({}/{}): ", + "Proceed?".yellow(), + "Y".bold().green(), + "N".red()); + + let response = query(&prompt, buffer, mag); + match response.as_str() { + "y" | "Y" | "" => println!(""), + "n" | "N" => clear_exit(), + _ => { + buffer.clear(); + println!( + "{}; please enter {} or {}.", + "Invalid response".bold().red(), + "Y".bold().green(), + "N".bold().red()); + check_directory(buffer, mag); + }, + } +} + +fn clear_exit() { + let _ = + std::process::Command::new("clear") + .status(); + std::process::exit(0); +} + +/// helper fn for get_language; handles queried response +fn route_response(buffer: &mut String, response: String, mag: &str, proj_name: &String) { + match response.as_str() { + "ls" => list_languages(buffer, mag), + "rust" => rust::init(buffer, mag, proj_name), + "js" => js::init(buffer), + "ts" => ts::init(buffer), + "react" => react::init(buffer), + "vue" => vue::init(buffer), + "svelte" => svelte::init(buffer), + "zig" => zig::init(buffer), + _ => { + buffer.clear(); + println!( + "{}; please enter a valid language.\n{} {} {}", + "Invalid response".bold().red(), + "(or type".black().dimmed(), + "ls".purple(), + "to list all valid languages)".black().dimmed()); + get_language(buffer, mag); + }, + } +} + +/// query user for the project language, then run the corresponding language_init() function +pub fn get_language(buffer: &mut String, mag: &str) -> String { + buffer.clear(); + let prompt = format!( + "{} {} {}: ", + "What".yellow(), + "language".cyan().bold(), + "is this project for?".yellow()); + query(&prompt, buffer, mag).to_lowercase() +} + +/// called when 'ls' is given as a response to `get_language()` +fn list_languages(buffer: &mut String, mag: &str) { + println!("{}", + "rust\njs\nts\nreact\nvue\nsvelte\n" + .blue() + .dimmed()); + get_language(buffer, mag); +} + +/// queries user for project name, returns an owned string +pub fn get_project_name(buffer: &mut String, mag: &str) -> String { + buffer.clear(); + let prompt = format!( + "{}{}: ", + "Please enter the ".yellow(), + "project name".cyan().bold()); + query(&prompt, buffer, mag).to_string() +} + +/// creates a README.md in the target directory in a useful format +pub fn gen_readme(proj_name: &String, dir: &std::path::Path) -> Result<()>{ + let mut base_readme: String = r#"# + +**[Short, memorable description of your project]** + +## Table of Contents + +* [Installation](#installation) +* [Usage](#usage) +* [Contributing](#contributing) +* [License](#license) +* [Additional Information](#additional-information) + +## Installation + +**Clearly describe how to install your project.** This may involve specifying dependencies, prerequisites, and build instructions. Use code blocks, links, and step-by-step guides for clarity. + +## Usage + +**Provide clear and concise instructions on how to use your project.** Explain its functionalities, features, and common use cases. Include examples, screenshots, or GIFs if helpful. + +**Tips:** + +* Break down instructions into logical steps. +* Use bullet points for succinct explanations. +* Consider creating a separate "Getting Started" guide for beginners. + +## Contributing + +**Outline your contribution guidelines.** Explain how users can contribute to your project, whether through code, bug reports, or documentation improvements. Specify preferred code style, pull request format, and testing procedures. + +## License + +**Specify the license under which your project is distributed.** Use clear and concise language, and link to the full license text in the `LICENSE` file. + +## Additional Information + +**Include any other relevant information you want to share.** This could be links to related projects, documentation, support channels, or your contact information. + +**Remember:** + +* Keep your README.md file concise and focused. +* Use clear headings, formatting, and visuals for readability. +* Update your README.md file regularly to reflect changes in your project. +"#.to_string(); + base_readme.insert_str(2, proj_name); + let mut file = fs::File::create(dir)?; + file.write_all(base_readme.as_bytes()) +} diff --git a/src/libs/js.rs b/src/libs/js.rs new file mode 100644 index 0000000..780b959 --- /dev/null +++ b/src/libs/js.rs @@ -0,0 +1,4 @@ +pub fn init(buffer: &mut String) { + todo!("JavaScript init script needed"); +} + diff --git a/src/libs/mod.rs b/src/libs/mod.rs new file mode 100644 index 0000000..8d574f7 --- /dev/null +++ b/src/libs/mod.rs @@ -0,0 +1,8 @@ +pub mod base; +pub mod js; +pub mod react; +pub mod rust; +pub mod svelte; +pub mod ts; +pub mod vue; +pub mod zig; diff --git a/src/libs/react.rs b/src/libs/react.rs new file mode 100644 index 0000000..957f2cd --- /dev/null +++ b/src/libs/react.rs @@ -0,0 +1,4 @@ +pub fn init(buffer: &mut String) { + todo!("React init script needed"); +} + diff --git a/src/libs/rust.rs b/src/libs/rust.rs new file mode 100644 index 0000000..ff627a3 --- /dev/null +++ b/src/libs/rust.rs @@ -0,0 +1,50 @@ +use colored::*; +use crate::libs::base::{query, gen_readme}; + +fn get_is_lib(buffer: &mut String, mag: &str) -> String { + buffer.clear(); + let prompt = format!( + "{} {} {} {} {}: ", + "Is this a".yellow(), + "binary".cyan().bold(), + "or".yellow(), + "library".cyan().bold(), + "project?".yellow()); + let response = query(&prompt, buffer, mag); + match response.as_str() { + "b" | "bin" | "binary" => "--bin".to_string(), + "l" | "lib" | "library" => "--lib".to_string(), + _ => get_is_lib(buffer, mag), + } +} + +pub fn init(buffer: &mut String, mag: &str, proj_name: &String) { + let is_lib = get_is_lib(buffer, mag); + let cwd = std::env::current_dir() + .expect("Error getting CWD") + .as_path() + .as_os_str() + .to_str() + .unwrap() + .to_owned(); + println!( + "{}{}{}{}{}", + "Creating Rust project ".purple().dimmed(), + proj_name.purple().bold(), + " in ".purple().dimmed(), + cwd.purple(), + "...".purple().dimmed()); + let mut handle = std::process::Command::new("cargo") + .args(["new", proj_name, &is_lib, "--quiet"]) + .spawn() + .expect("Error spawning child process."); + let exit_status = handle.wait().unwrap(); + let proj_dir = format!("./{}", proj_name); + let readme_dir = proj_dir + "/README.md"; + let readme_path = std::path::Path::new(&readme_dir); + gen_readme(&proj_name, &readme_path).unwrap(); + if exit_status.success() { + println!("{}", "Done!".green().bold()); + } +} + diff --git a/src/libs/svelte.rs b/src/libs/svelte.rs new file mode 100644 index 0000000..60e0db1 --- /dev/null +++ b/src/libs/svelte.rs @@ -0,0 +1,3 @@ +pub fn init(buffer: &mut String) { + todo!("Svelte init script needed"); +} diff --git a/src/libs/ts.rs b/src/libs/ts.rs new file mode 100644 index 0000000..000c567 --- /dev/null +++ b/src/libs/ts.rs @@ -0,0 +1,4 @@ +pub fn init(buffer: &mut String) { + todo!("TypeScript init script needed"); +} + diff --git a/src/libs/vue.rs b/src/libs/vue.rs new file mode 100644 index 0000000..ce8b74a --- /dev/null +++ b/src/libs/vue.rs @@ -0,0 +1,4 @@ +pub fn init(buffer: &mut String) { + todo!("Vue init script needed"); +} + diff --git a/src/libs/zig.rs b/src/libs/zig.rs new file mode 100644 index 0000000..08d9220 --- /dev/null +++ b/src/libs/zig.rs @@ -0,0 +1,4 @@ +pub fn init(buffer: &mut String) { + todo!("Zig init script needed"); +} + diff --git a/src/main.rs b/src/main.rs index 7b99863..3a8359c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,49 +1,44 @@ mod libs; +use libs::base::start; +use clap::builder::styling::{AnsiColor, Effects, Styles}; +use clap::Parser; -use libs::*; -use colored::*; +/* Argument parsing */ -fn main() { - let mut user_input = String::new(); - title(); - check_directory(&mut user_input); +/// Help text styling +fn styles() -> Styles { + Styles::styled() + .header(AnsiColor::Yellow.on_default() | Effects::BOLD) + .usage(AnsiColor::Yellow.on_default() | Effects::BOLD) + .literal(AnsiColor::Blue.on_default()) + .placeholder(AnsiColor::Green.on_default()) } -fn title() { - println!("{}", "┍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┑".black().dimmed()); - println!( - "{}{}{}{}{}", - "│".black().dimmed(), - " Project Initializer".bold(), - " by".normal(), - " uptu".bold().cyan(), - " │".black().dimmed()); - println!("{}", "┕━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙".black().dimmed()); - println!("{}", "Please ensure you are in the correct directory and branch before proceeding.".bold().red()); - let cwd = std::env::current_dir() - .expect("Error getting CWD") - .as_path() - .as_os_str() - .to_str() - .unwrap() - .to_owned(); - println!("{}{}","CWD: ".dimmed(), cwd.purple()); +/// The --help string +const HELP: &'static str = "\x1b[0;1;92;4mPi\x1b[0m is a CLI \x1b[34mproject initializer\x1b[0m which uses per-language build tools to create project directories populated with a full project template."; + +/// The arguments to be pushed to the rest of the program +#[derive(Parser, Debug)] +#[command(version, author = "\x1b[0;93muptu\x1b[0m, \x1b[33muptu@uptu.dev\x1b[0m", about = HELP, styles = styles(), long_about = None, next_line_help = true, help_template = "\ +{before-help}\x1b[1;95;4m{name}\x1b[0;95;4m {version}\x1b[0m +{author-with-newline} +{about-with-newline} +{usage-heading} {usage} + +{all-args}{after-help} +")] +struct Args { + /// The name of the project + #[arg(name = "NAME", short = 'n', long = "name", verbatim_doc_comment)] + name: Option, + + /// The target language of the project + #[arg(name = "LANGUAGE", short = 'l', long = "lang", verbatim_doc_comment)] + lang: Option, } -fn check_directory(user_input_buffer: &mut String) { - let prompt = format!( - "{} ({}/{}): ", - "Proceed?".yellow(), - "Y".bold().green(), - "N".red()); - query(&prompt, user_input_buffer); - - match &*(*user_input_buffer).trim() { // this line is disgusting - "y" | "Y" => println!(""), - "n" | "N" => return, - _ => { - user_input_buffer.clear(); - check_directory(user_input_buffer); - }, - } + +fn main() { + let args = Args::parse(); + start(args.name, args.lang); }