1use clap::Parser;
2use std::{
3 fs::{self},
4 path::PathBuf,
5};
6
7const TEMPLATE_MAIN: &str = include_str!("../templates/main.c.template");
9const TEMPLATE_HEADER: &str = include_str!("../templates/header.h.template");
10const TEMPLATE_MAKEFILE: &str = include_str!("../templates/Makefile.template");
11const TEMPLATE_README: &str = include_str!("../templates/README.md.template");
12const TEMPLATE_GITIGNORE: &str = include_str!("../templates/gitignore.template");
13const TEMPLATE_TEST: &str = include_str!("../templates/test.c.template");
14
15#[derive(Debug)]
16pub enum PriccError {
17 ConfigError(String),
18 IoError(std::io::Error),
19 TemplateError(String),
20}
21
22impl std::error::Error for PriccError {}
23impl std::fmt::Display for PriccError {
24 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
25 match self {
26 PriccError::ConfigError(msg) => write!(f, "Configuration error: {}", msg),
27 PriccError::IoError(e) => write!(f, "IO error: {}", e),
28 PriccError::TemplateError(msg) => write!(f, "Template error: {}", msg),
29 }
30 }
31}
32
33impl From<std::io::Error> for PriccError {
34 fn from(error: std::io::Error) -> Self {
35 PriccError::IoError(error)
36 }
37}
38
39#[derive(Debug)]
40pub struct PriccConfig {
41 pub name: String,
42 pub author: Option<String>,
43 pub proj_version: String,
44 pub description: Option<String>,
45 pub c_standard: CStandard,
46 pub include_tests: bool,
47}
48
49#[derive(Debug, Clone, Copy, clap::ValueEnum)]
50pub enum CStandard {
51 C89,
52 C99,
53 C11,
54 C17,
55}
56
57impl CStandard {
58 fn to_flag(&self) -> &'static str {
59 match self {
60 CStandard::C89 => "-std=c89",
61 CStandard::C99 => "-std=c99",
62 CStandard::C11 => "-std=c11",
63 CStandard::C17 => "-std=c17",
64 }
65 }
66}
67
68impl Default for PriccConfig {
69 fn default() -> Self {
70 Self {
71 name: String::new(),
72 author: None,
73 proj_version: "0.1.0".to_string(),
74 description: None,
75 c_standard: CStandard::C11,
76 include_tests: false,
77 }
78 }
79}
80
81impl PriccConfig {
82 pub fn new(name: String) -> Result<Self, PriccError> {
83 if name.is_empty() {
84 return Err(PriccError::ConfigError(
85 "Project name cannot be empty".to_string(),
86 ));
87 }
88
89 Ok(Self {
90 name,
91 ..Default::default()
92 })
93 }
94
95 pub fn with_author(mut self, author: String) -> Self {
96 self.author = Some(author);
97 self
98 }
99
100 pub fn with_tests(mut self) -> Self {
101 self.include_tests = true;
102 self
103 }
104}
105
106struct Template<'a> {
107 content: &'a str,
108}
109
110impl<'a> Template<'a> {
111 fn new(content: &'a str) -> Self {
112 Self { content }
113 }
114
115 fn render(&self, vars: &[(&str, &str)]) -> String {
116 let mut result = self.content.to_string();
117 for (key, value) in vars {
118 result = result.replace(&format!("{{{{{}}}}}", key), value);
119 }
120 result
121 }
122}
123
124pub struct ProjectBuilder {
125 config: PriccConfig,
126 root_dir: PathBuf,
127}
128
129impl ProjectBuilder {
130 pub fn new(config: PriccConfig) -> Self {
131 Self {
132 root_dir: PathBuf::from(&config.name),
133 config,
134 }
135 }
136
137 pub fn build(&self) -> Result<(), PriccError> {
138 self.create_directory_structure()?;
139 self.create_source_files()?;
140 self.create_build_files()?;
141 self.create_docs()?;
142 if self.config.include_tests {
143 self.create_test_structure()?;
144 }
145 self.init_git()?;
146
147 Ok(())
148 }
149
150 fn create_directory_structure(&self) -> Result<(), PriccError> {
151 let dirs = ["src", "include", "docs", "build", "bin"];
152
153 for dir in dirs.iter() {
154 fs::create_dir_all(self.root_dir.join(dir))?;
155 }
156
157 Ok(())
158 }
159
160 fn create_source_files(&self) -> Result<(), PriccError> {
161 let main_template = Template::new(TEMPLATE_MAIN);
163 let main_content = main_template.render(&[
164 ("name", &self.config.name),
165 (
166 "author",
167 self.config.author.as_deref().unwrap_or("Anonymous"),
168 ),
169 (
170 "description",
171 self.config.description.as_deref().unwrap_or("A C project"),
172 ),
173 ]);
174
175 fs::write(self.root_dir.join("src").join("main.c"), main_content)?;
176
177 let header_template = Template::new(TEMPLATE_HEADER);
179 let header_content = header_template.render(&[
180 ("name", &self.config.name),
181 ("guard", &self.config.name.to_uppercase()),
182 (
183 "author",
184 self.config.author.as_deref().unwrap_or("Anonymous"),
185 ),
186 (
187 "description",
188 self.config.description.as_deref().unwrap_or("A C project"),
189 ),
190 ]);
191
192 fs::write(
193 self.root_dir
194 .join("include")
195 .join(format!("{}.h", self.config.name)),
196 header_content,
197 )?;
198
199 Ok(())
200 }
201
202 fn create_build_files(&self) -> Result<(), PriccError> {
203 let makefile_template = Template::new(TEMPLATE_MAKEFILE);
204 let makefile_content = makefile_template.render(&[
205 ("name", &self.config.name),
206 ("cstandard", self.config.c_standard.to_flag()),
207 ]);
208
209 fs::write(self.root_dir.join("Makefile"), makefile_content)?;
210
211 Ok(())
212 }
213
214 fn create_docs(&self) -> Result<(), PriccError> {
215 let readme_template = Template::new(TEMPLATE_README);
216 let readme_content = readme_template.render(&[
217 ("name", &self.config.name),
218 (
219 "description",
220 self.config.description.as_deref().unwrap_or("A C project"),
221 ),
222 (
223 "author",
224 self.config.author.as_deref().unwrap_or("Anonymous"),
225 ),
226 ("version", &self.config.proj_version),
227 ]);
228
229 fs::write(self.root_dir.join("README.md"), readme_content)?;
230
231 Ok(())
232 }
233
234 fn create_test_structure(&self) -> Result<(), PriccError> {
235 fs::create_dir_all(self.root_dir.join("tests"))?;
236
237 let test_template = Template::new(TEMPLATE_TEST);
238 let test_content = test_template.render(&[("name", &self.config.name)]);
239
240 fs::write(
241 self.root_dir.join("tests").join("test_main.c"),
242 test_content,
243 )?;
244
245 Ok(())
246 }
247
248 fn init_git(&self) -> Result<(), PriccError> {
249 let gitignore_template = Template::new(TEMPLATE_GITIGNORE);
250 let gitignore_content = gitignore_template.render(&[("name", &self.config.name)]);
251
252 fs::write(self.root_dir.join(".gitignore"), gitignore_content)?;
253
254 std::process::Command::new("git")
255 .arg("init")
256 .current_dir(&self.root_dir)
257 .output()
258 .map_err(|e| PriccError::IoError(e))?;
259
260 Ok(())
261 }
262}
263
264#[derive(Parser)]
265#[command(
266 name = "pricc",
267 about = "A C project generator",
268 version,
269 author = "Your Name <your.email@example.com>"
270)]
271pub struct Cli {
272 #[arg(required = true)]
274 name: String,
275
276 #[arg(short, long)]
278 author: Option<String>,
279
280 #[arg(short, long)]
282 description: Option<String>,
283
284 #[arg(short, long, value_enum, default_value_t = CStandard::C11)]
286 standard: CStandard,
287
288 #[arg(short, long)]
290 tests: bool,
291
292 #[arg(short = 'v', long = "proj-version", default_value = "0.1.0")]
294 proj_version: String,
295}
296
297impl From<Cli> for PriccConfig {
298 fn from(cli: Cli) -> Self {
299 let mut config = PriccConfig::new(cli.name).expect("Invalid project name");
300
301 if let Some(author) = cli.author {
302 config = config.with_author(author);
303 }
304
305 if cli.tests {
306 config = config.with_tests();
307 }
308
309 config.description = cli.description;
310 config.proj_version = cli.proj_version;
311 config.c_standard = cli.standard;
312
313 config
314 }
315}
316
317pub fn run() -> Result<(), PriccError> {
318 let cli = Cli::parse();
319 let config: PriccConfig = cli.into();
320 let builder = ProjectBuilder::new(config);
321 builder.build()
322}