source: gignore/src/template.rs

trunk
Last change on this file was 3cf1172, checked in by Asher Mullaney <asher.mullaney@…>, 10 days ago

Resolved #4. Also prettified 'Continue anyway' prompt.

  • Property mode set to 100644
File size: 10.6 KB
Line 
1// IMPORTANT -- LEGAL NOTICE!
2
3// gignore: a .gitignore manipulation program
4// Copyright (C) 2026 Asher Mullaney \<asher.mullaney@gmail.com\>
5//
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10//
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15//
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19use std::{
20 fs,
21 io::{self, Write},
22 os::unix::fs::MetadataExt,
23 path::{Path, PathBuf},
24 process,
25};
26
27use anyhow::{Context, Error, Result};
28
29use serde::{Deserialize, Serialize};
30
31use super::{open_append, open_truncate};
32
33#[derive(Serialize, Deserialize, Debug)]
34pub(crate) struct Template {
35 source_file: PathBuf,
36 name: String,
37 append: bool,
38}
39
40impl Template {
41 pub(crate) fn new(source_file: &Path, name: &str, append: bool) -> Self {
42 Template {
43 source_file: source_file.to_owned(),
44 name: String::from(name),
45 append,
46 }
47 }
48
49 fn get_source_file(&self) -> &PathBuf {
50 &self.source_file
51 }
52 fn change_source_file<T: AsRef<Path>>(&mut self, new: T) {
53 self.source_file = new.as_ref().to_owned();
54 }
55 pub(crate) fn get_name(&self) -> &str {
56 &self.name
57 }
58 pub(crate) fn is_append(&self) -> bool {
59 self.append
60 }
61 fn save(&self) -> Result<()> {
62 let path = "/usr/lib/gignore/gignore_templates";
63
64 if !fs::exists(path).context("cannot search for `/usr/lib/gignore/gignore_templates`")? {
65 return Err(Error::msg(
66 "`/usr/lib/gignore/gignore_templates` does not exist",
67 ));
68 }
69
70 let mut file_handle = open_append(path.as_ref())
71 .context("cannot open `/usr/lib/gignore/gignore_templates`")?;
72
73 let write_string = serde_json::to_string(&self)
74 .context("cannot convert template to JSON using `serde_json`")?;
75
76 file_handle
77 .write_all(format!("{write_string}\n").as_bytes())
78 .context("cannot write to `/usr/lib/gignore/gignore_templates`")?;
79
80 Ok(())
81 }
82}
83
84fn lib_read() -> Result<Vec<Template>> {
85 let path = "/usr/lib/gignore/gignore_templates";
86
87 let mut template_vec: Vec<Template> = Vec::new();
88
89 if fs::read_to_string(path)
90 .context("cannot read `/usr/lib/gignore/gignore_templates`")?
91 .is_empty()
92 {
93 return Ok(template_vec);
94 }
95
96 for line in fs::read_to_string(path)
97 .context("cannot read `/usr/lib/gignore/gignore_templates`")?
98 .lines()
99 {
100 template_vec.push(
101 serde_json::from_str(&line.trim())
102 .context("cannot convert JSON to `template` instance")?,
103 );
104 }
105 Ok(template_vec)
106}
107
108pub(crate) fn find_name(name: &str) -> Result<Option<Template>> {
109 let template_vec: Vec<Template> = lib_read().context("cannot read template library")?;
110
111 for item in template_vec {
112 if item.get_name() == name {
113 return Ok(Some(item));
114 }
115 }
116 Ok(None)
117}
118
119pub(crate) fn remove(name: &str) -> Result<()> {
120 let path = "/usr/lib/gignore/gignore_templates";
121
122 let mut template_vec: Vec<Template> = lib_read().context("cannot read template library")?;
123
124 template_vec
125 .extract_if(.., |item| item.get_name() == name)
126 .for_each(drop);
127
128 let template = match find_name(name).context(format!(
129 "cannot search for name {name} in /usr/lib/gignore/gignore_templates"
130 ))? {
131 None => {
132 eprintln!(
133 "No template exists in `/usr/lib/gignore/gignore_templates` under the name `{name}`."
134 );
135 process::exit(0);
136 }
137 Some(t) => t,
138 };
139
140 clear().context("cannot clear template library file")?;
141
142 if template_vec.is_empty() {
143 println!("{path} is now empty.");
144 fs::write(path, b"").context("cannot write to `/usr/lib/gignore/gignore_templates`")?;
145 } else {
146 for item in template_vec {
147 item.save().context("cannot save template")?;
148 }
149 }
150
151 fs::remove_file(template.get_source_file()).context(format!(
152 "cannot remove `{}`",
153 template.get_source_file().display()
154 ))?;
155 Ok(())
156}
157
158pub(crate) fn add(mut template: Template) -> Result<()> {
159 if !fs::exists(template.get_source_file())
160 .context(format!("cannot search for `{}`", template.get_name()))?
161 {
162 return Err(Error::msg(format!(
163 "The source file specified, `{}`, does not exist.",
164 template.get_source_file().display()
165 )));
166 }
167 let extension = (*(template.get_source_file()))
168 .to_str()
169 .expect("FATAL: Could not convert source_file to string!")
170 .split_terminator('.')
171 .nth(1);
172
173 dbg!(extension);
174
175 if extension != Some("ggnr") {
176 eprint!(
177 "WARNING: specified source file, `{}`, does not have the file extension for gignore templates (`.ggnr`). Continue anyway? [y/N] ",
178 template.get_source_file().display()
179 );
180
181 let mut buf = String::new();
182 io::stdin()
183 .read_line(&mut buf)
184 .context("cannot read from stdin")?;
185
186 match &*buf.to_lowercase().trim() {
187 "y" => {
188 eprintln!("Continuing...");
189 }
190 _ => {
191 eprintln!("User denied operation. Exiting.");
192 process::exit(0);
193 }
194 }
195 };
196
197 let new_template_path = format!("/usr/lib/gignore/{}", template.get_source_file().display());
198
199 if fs::exists(&new_template_path).context(format!("cannot search for `{new_template_path}`"))? {
200 eprint!("WARNING: the file `{new_template_path}` already exists. Overwrite it? [y/N] ");
201
202 let mut buf = String::new();
203 io::stdin()
204 .read_line(&mut buf)
205 .context("cannot read from stdin")?;
206
207 match &*buf.to_lowercase().trim() {
208 "y" => {
209 eprintln!("Continuing...");
210
211 fs::remove_file(&new_template_path)
212 .context(format!("cannot remove {new_template_path}"))?
213 }
214 _ => {
215 eprintln!("User denied operation. Exiting.");
216 process::exit(0);
217 }
218 };
219 };
220
221 drop(
222 fs::File::create_new(&new_template_path)
223 .context(format!("cannot create file `{new_template_path}`"))?,
224 );
225
226 let bytes_written =
227 fs::copy(template.get_source_file(), &*new_template_path).context(format!(
228 "cannot copy bytes from `{}` to `{new_template_path}`",
229 template.get_source_file().display()
230 ))?;
231 if !bytes_written
232 == fs::metadata(template.get_source_file())
233 .context(format!(
234 "cannot read metadata of `{}`",
235 template.get_source_file().display()
236 ))?
237 .size()
238 {
239 eprintln!(
240 "WARNING: Not all bytes of `{}` were written to `{new_template_path}`. ",
241 template.get_source_file().display()
242 );
243 }
244
245 if find_name(template.get_name())
246 .context("cannot search for name in template library")?
247 .is_some()
248 {
249 return Err(Error::msg(format!(
250 "A template with the name `{}` already exists!",
251 template.get_name()
252 )));
253 }
254 template.change_source_file(new_template_path);
255
256 template.save().context("cannot save template")?;
257
258 Ok(())
259}
260
261fn get_template_contents(name: &str) -> Result<String> {
262 let template = find_name(name).context("cannot search for name in template library")?;
263
264 Ok(fs::read_to_string(
265 match template {
266 Some(ref template) => template,
267 None => return Err(Error::msg(format!("No template under name `{name}`"))),
268 }
269 .get_source_file(),
270 )
271 .context(format!(
272 "cannot read {}",
273 template.unwrap().get_source_file().display()
274 ))?)
275}
276
277pub(crate) fn clear() -> Result<()> {
278 let path = "/usr/lib/gignore/gignore_templates";
279
280 open_truncate(path.as_ref())
281 .context("cannot open `/usr/lib/gignore/gignore_templates` in `truncate` mode")?
282 .write_all(b"")
283 .context("cannot write to /usr/lib/gignore/gignore_templates")?;
284
285 Ok(())
286}
287
288pub(crate) fn gitignore_write(name: &str) -> Result<()> {
289 let template = find_name(name).context("cannot search for name in template library")?;
290
291 if match template {
292 Some(template) => template.is_append(),
293 None => return Err(Error::msg(format!("No template under name `{name}`"))),
294 } {
295 let mut file_handle = open_append(".gitignore".as_ref())
296 .context("cannot open `.gitignore` in `append` mode")?;
297
298 file_handle
299 .write_all(
300 get_template_contents(name)
301 .context("cannot retrieve contents of template")?
302 .as_bytes(),
303 )
304 .context("cannot write to `.gitignore`")?;
305
306 return Ok(());
307 }
308
309 let mut file_handle = open_truncate(".gitignore".as_ref())
310 .context("cannot open `.gitignore` in `truncate` mode")?;
311
312 file_handle
313 .write_all(
314 get_template_contents(name)
315 .context("cannot retrieve contents of template")?
316 .as_bytes(),
317 )
318 .context("cannot write to `.gitignore`")?;
319
320 Ok(())
321}
322
323pub(crate) fn list(oneline: bool) -> Result<()> {
324 if !oneline {
325 for item in lib_read().context("cannot read template library")? {
326 if item.is_append() {
327 println!(
328 "{}: {} <append>",
329 item.get_name(),
330 item.get_source_file().display()
331 );
332 } else {
333 println!("{}: {}", item.get_name(), item.get_source_file().display());
334 }
335 }
336 return Ok(());
337 }
338
339 println!();
340
341 for item in lib_read().context("cannot read template library")? {
342 if item.is_append() {
343 print!(
344 "{}: {} (append), ",
345 item.get_name(),
346 item.get_source_file().display()
347 );
348 } else {
349 print!(
350 "{}: {}, ",
351 item.get_name(),
352 item.get_source_file().display()
353 );
354 }
355 }
356 println!();
357
358 Ok(())
359}
Note: See TracBrowser for help on using the repository browser.