1use std::{
3 fs::{self, OpenOptions},
4 io,
5 path::Path,
6};
7
8use thiserror::Error;
9
10pub mod formats;
11pub use formats::Format;
12use universe::Universe;
13pub mod universe;
14
15#[derive(Error, Debug)]
16pub enum ConwayError {
17 #[error("IO error: {0}")]
18 Io(#[from] io::Error),
19
20 #[error("Format detection error: {0}")]
21 FormatDetection(String),
22
23 #[error("Parsing error: {0}")]
24 Parsing(String),
25
26 #[error("Writing error: {0}")]
27 Writing(String),
28}
29
30pub type Result<T> = std::result::Result<T, ConwayError>;
31
32pub fn detect_format(content: &str) -> Result<Format> {
34 for line in content.lines() {
36 let trimmed = line.trim_start();
37 if trimmed.starts_with('#') || trimmed.is_empty() {
38 continue;
39 }
40
41 if formats::rle::is_valid_header(trimmed) {
42 return Ok(Format::Rle);
43 }
44
45 return Ok(Format::Plaintext);
46 }
47
48 Err(ConwayError::FormatDetection(
49 "Could not detect file format".into(),
50 ))
51}
52
53pub fn parse_file(input_path: &Path) -> Result<(Format, Universe)> {
55 let content = fs::read_to_string(input_path)?;
56 parse_text(content.as_str())
57}
58
59pub fn parse_text(input: &str) -> Result<(Format, Universe)> {
61 let input_format = detect_format(input)?;
62 Ok((input_format, formats::parse(input, input_format)?))
63}
64
65pub fn convert_file(input_path: &Path, output_path: &Path, force: bool) -> Result<()> {
67 let mut open_options = OpenOptions::new();
68 let open_options = open_options
69 .write(true)
70 .create(force)
71 .truncate(force)
72 .create_new(!force);
73
74 let mut output_file = open_options.open(output_path)?;
75
76 let (input_format, universe) = parse_file(input_path)?;
77
78 let target_format = match input_format {
79 Format::Rle => Format::Plaintext,
80 Format::Plaintext => Format::Rle,
81 };
82
83 formats::write(&universe, &mut output_file, target_format)?;
84
85 Ok(())
86}
87
88pub(crate) fn filter_comment_lines(content: &str, starting_char: char) -> Vec<&str> {
90 content
91 .lines()
92 .map(str::trim)
93 .filter(|line| !line.starts_with(starting_char) && !line.is_empty())
94 .collect()
95}