Skip to main content

glyph_cli/
lib.rs

1mod commands;
2mod error;
3
4use clap::Parser;
5use regex::Regex;
6use std::env;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use commands::{create_glyph, install, play_glyph, uninstall};
11use commands::{Cli, Commands};
12pub use error::{CliError, Result};
13
14fn extract_frame_number(filename: &str) -> Option<u32> {
15    // Try to find a number sequence in the filename
16    let re = Regex::new(r"(\d+)").unwrap();
17    re.find_iter(filename)
18        .last() // Take the last number sequence (usually the frame number)
19        .and_then(|m| m.as_str().parse::<u32>().ok())
20}
21
22fn resolve_path(path: &Path) -> Result<PathBuf> {
23    if path.is_absolute() {
24        Ok(path.to_path_buf())
25    } else {
26        Ok(env::current_dir()?.join(path))
27    }
28}
29
30fn find_sequence_frames(first_frame: &Path) -> Result<Vec<PathBuf>> {
31    let first_frame = resolve_path(first_frame)?;
32
33    // Check if path exists before proceeding
34    if !first_frame.exists() {
35        return Err(CliError::File(format!(
36            "Path does not exist: {}",
37            first_frame.display()
38        )));
39    }
40
41    // If it's a directory, look for frame sequences inside
42    if first_frame.is_dir() {
43        let dir_entries = fs::read_dir(&first_frame)?;
44        let mut frames = Vec::new();
45
46        // Collect all potential frame files
47        for entry in dir_entries {
48            let entry = entry?;
49            if entry.file_type()?.is_file() {
50                frames.push(entry.path());
51            }
52        }
53
54        if frames.is_empty() {
55            return Err(CliError::File(format!(
56                "No files found in directory {}",
57                first_frame.display()
58            )));
59        }
60
61        // Sort frames by name
62        frames.sort();
63        Ok(frames)
64    } else {
65        // If it's a file, try to find other files in the same directory with similar names
66        let parent = first_frame
67            .parent()
68            .ok_or_else(|| CliError::File("Invalid path: no parent directory".to_string()))?;
69
70        // Check if parent directory exists
71        if !parent.exists() {
72            return Err(CliError::File(format!(
73                "Parent directory does not exist: {}",
74                parent.display()
75            )));
76        }
77
78        let first_frame_name = first_frame
79            .file_name()
80            .ok_or_else(|| CliError::File("Invalid filename".to_string()))?
81            .to_string_lossy();
82
83        // Get the base pattern by removing the number sequence
84        let base_pattern = first_frame_name
85            .split_once(char::is_numeric)
86            .map(|(prefix, _)| prefix)
87            .unwrap_or(&first_frame_name);
88
89        let extension = first_frame
90            .extension()
91            .and_then(|ext| ext.to_str())
92            .unwrap_or("");
93
94        let mut frames = Vec::new();
95        for entry in fs::read_dir(parent)? {
96            let entry = entry?;
97            let path = entry.path();
98
99            if path.is_file() {
100                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
101                    // Check if the file has the same extension and starts with the base pattern
102                    if name.starts_with(base_pattern)
103                        && path.extension().and_then(|ext| ext.to_str()) == Some(extension)
104                    {
105                        frames.push(path);
106                    }
107                }
108            }
109        }
110
111        if frames.is_empty() {
112            // If no sequence found and the file doesn't exist, return error
113            if !first_frame.exists() {
114                return Err(CliError::File(format!(
115                    "File does not exist: {}",
116                    first_frame.display()
117                )));
118            }
119            // If file exists but no sequence found, just use the single frame
120            Ok(vec![first_frame])
121        } else {
122            // Sort frames by their frame number
123            frames.sort_by_key(|path| {
124                path.file_name()
125                    .and_then(|name| name.to_str())
126                    .and_then(extract_frame_number)
127                    .unwrap_or(0)
128            });
129            Ok(frames)
130        }
131    }
132}
133
134pub fn main() -> Result<()> {
135    let cli = Cli::parse();
136
137    match cli.command {
138        Some(Commands::Create {
139            input,
140            output,
141            duration,
142        }) => {
143            let input_files = find_sequence_frames(&input).map_err(|e| {
144                CliError::File(format!(
145                    "Failed to find frame sequence in {}: {}",
146                    input.display(),
147                    e
148                ))
149            })?;
150
151            if input_files.is_empty() {
152                return Err(CliError::Input("No frame files found".to_string()));
153            }
154
155            println!("Found {} frames", input_files.len());
156            for (i, path) in input_files.iter().enumerate() {
157                if let Some(name) = path.file_name() {
158                    println!("  Frame {}: {}", i, name.to_string_lossy());
159                }
160            }
161
162            create_glyph(input_files, output, duration)
163        }
164        Some(Commands::Install) => {
165            println!("Installing Glyph system-wide...");
166            install::install()?;
167            println!("Installation complete! Please restart your shell to use the new features.");
168            Ok(())
169        }
170        Some(Commands::Uninstall) => {
171            println!("Uninstalling Glyph...");
172            uninstall::uninstall()?;
173            println!("Uninstallation complete! Please restart your shell.");
174            Ok(())
175        }
176        None => {
177            // Default to play mode if a file is provided
178            if let Some(input) = cli.input {
179                if input.extension().and_then(|ext| ext.to_str()) == Some("glyph") {
180                    play_glyph(input, cli.loops)
181                } else {
182                    Err(CliError::Input(
183                        "Input file must have .glyph extension".to_string(),
184                    ))
185                }
186            } else {
187                Err(CliError::Input(
188                    "Please provide a .glyph file to play or use a subcommand".to_string(),
189                ))
190            }
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use std::fs::File;
199    use std::io::Write;
200    use tempfile::TempDir;
201
202    fn create_test_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
203        let path = dir.path().join(name);
204        let mut file = File::create(&path).unwrap();
205        writeln!(file, "{}", content).unwrap();
206        path
207    }
208
209    #[test]
210    fn test_extract_frame_number() {
211        assert_eq!(extract_frame_number("frame_001.txt"), Some(1));
212        assert_eq!(extract_frame_number("frame_1.txt"), Some(1));
213        assert_eq!(extract_frame_number("frame.001.txt"), Some(1));
214        assert_eq!(extract_frame_number("frame99.txt"), Some(99));
215        assert_eq!(extract_frame_number("noframe.txt"), None);
216    }
217
218    #[test]
219    fn test_find_sequence_simple_numbering() {
220        let temp_dir = TempDir::new().unwrap();
221
222        // Create test files
223        let frame1 = create_test_file(&temp_dir, "frame_1.txt", "test");
224        let _frame2 = create_test_file(&temp_dir, "frame_2.txt", "test");
225        let _frame3 = create_test_file(&temp_dir, "frame_3.txt", "test");
226
227        let frames = find_sequence_frames(&frame1).unwrap();
228        assert_eq!(frames.len(), 3);
229        assert!(frames.iter().all(|f| f.exists()));
230    }
231
232    #[test]
233    fn test_find_sequence_padded_numbering() {
234        let temp_dir = TempDir::new().unwrap();
235
236        // Create test files
237        let frame1 = create_test_file(&temp_dir, "frame_001.txt", "test");
238        let _frame2 = create_test_file(&temp_dir, "frame_002.txt", "test");
239        let _frame3 = create_test_file(&temp_dir, "frame_003.txt", "test");
240
241        let frames = find_sequence_frames(&frame1).unwrap();
242        assert_eq!(frames.len(), 3);
243
244        // Verify correct ordering
245        let names: Vec<_> = frames
246            .iter()
247            .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
248            .collect();
249        assert_eq!(
250            names,
251            vec!["frame_001.txt", "frame_002.txt", "frame_003.txt"]
252        );
253    }
254
255    #[test]
256    fn test_find_sequence_mixed_files() {
257        let temp_dir = TempDir::new().unwrap();
258
259        // Create sequence files
260        let frame1 = create_test_file(&temp_dir, "frame_001.txt", "test");
261        create_test_file(&temp_dir, "frame_002.txt", "test");
262        create_test_file(&temp_dir, "frame_003.txt", "test");
263
264        // Create unrelated files
265        create_test_file(&temp_dir, "other.txt", "test");
266        create_test_file(&temp_dir, "unrelated_001.txt", "test");
267
268        let frames = find_sequence_frames(&frame1).unwrap();
269        assert_eq!(frames.len(), 3);
270
271        // Verify only sequence files are included
272        for frame in frames {
273            assert!(frame
274                .file_name()
275                .unwrap()
276                .to_string_lossy()
277                .starts_with("frame_"));
278        }
279    }
280
281    #[test]
282    fn test_find_sequence_directory() {
283        let temp_dir = TempDir::new().unwrap();
284
285        // Create test files in directory
286        create_test_file(&temp_dir, "frame_001.txt", "test");
287        create_test_file(&temp_dir, "frame_002.txt", "test");
288        create_test_file(&temp_dir, "other.txt", "test");
289
290        let frames = find_sequence_frames(temp_dir.path()).unwrap();
291        assert_eq!(frames.len(), 3); // All files in directory
292    }
293
294    #[test]
295    fn test_find_sequence_single_file() {
296        let temp_dir = TempDir::new().unwrap();
297        let single_file = create_test_file(&temp_dir, "single.txt", "test");
298
299        let frames = find_sequence_frames(&single_file).unwrap();
300        assert_eq!(frames.len(), 1);
301        assert_eq!(frames[0], single_file);
302    }
303
304    #[test]
305    fn test_find_sequence_nonexistent() {
306        let temp_dir = TempDir::new().unwrap();
307        let nonexistent = temp_dir.path().join("definitely_nonexistent_file.txt");
308        let result = find_sequence_frames(&nonexistent);
309        assert!(result.is_err());
310    }
311}