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 let re = Regex::new(r"(\d+)").unwrap();
17 re.find_iter(filename)
18 .last() .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 if !first_frame.exists() {
35 return Err(CliError::File(format!(
36 "Path does not exist: {}",
37 first_frame.display()
38 )));
39 }
40
41 if first_frame.is_dir() {
43 let dir_entries = fs::read_dir(&first_frame)?;
44 let mut frames = Vec::new();
45
46 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 frames.sort();
63 Ok(frames)
64 } else {
65 let parent = first_frame
67 .parent()
68 .ok_or_else(|| CliError::File("Invalid path: no parent directory".to_string()))?;
69
70 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 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 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 !first_frame.exists() {
114 return Err(CliError::File(format!(
115 "File does not exist: {}",
116 first_frame.display()
117 )));
118 }
119 Ok(vec![first_frame])
121 } else {
122 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 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 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 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 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 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_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 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_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); }
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}