mod commands;
mod error;
use clap::Parser;
use regex::Regex;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use commands::{create_glyph, install, play_glyph, uninstall};
use commands::{Cli, Commands};
pub use error::{CliError, Result};
fn extract_frame_number(filename: &str) -> Option<u32> {
let re = Regex::new(r"(\d+)").unwrap();
re.find_iter(filename)
.last() .and_then(|m| m.as_str().parse::<u32>().ok())
}
fn resolve_path(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(env::current_dir()?.join(path))
}
}
fn find_sequence_frames(first_frame: &Path) -> Result<Vec<PathBuf>> {
let first_frame = resolve_path(first_frame)?;
if !first_frame.exists() {
return Err(CliError::File(format!(
"Path does not exist: {}",
first_frame.display()
)));
}
if first_frame.is_dir() {
let dir_entries = fs::read_dir(&first_frame)?;
let mut frames = Vec::new();
for entry in dir_entries {
let entry = entry?;
if entry.file_type()?.is_file() {
frames.push(entry.path());
}
}
if frames.is_empty() {
return Err(CliError::File(format!(
"No files found in directory {}",
first_frame.display()
)));
}
frames.sort();
Ok(frames)
} else {
let parent = first_frame
.parent()
.ok_or_else(|| CliError::File("Invalid path: no parent directory".to_string()))?;
if !parent.exists() {
return Err(CliError::File(format!(
"Parent directory does not exist: {}",
parent.display()
)));
}
let first_frame_name = first_frame
.file_name()
.ok_or_else(|| CliError::File("Invalid filename".to_string()))?
.to_string_lossy();
let base_pattern = first_frame_name
.split_once(char::is_numeric)
.map(|(prefix, _)| prefix)
.unwrap_or(&first_frame_name);
let extension = first_frame
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
let mut frames = Vec::new();
for entry in fs::read_dir(parent)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with(base_pattern)
&& path.extension().and_then(|ext| ext.to_str()) == Some(extension)
{
frames.push(path);
}
}
}
}
if frames.is_empty() {
if !first_frame.exists() {
return Err(CliError::File(format!(
"File does not exist: {}",
first_frame.display()
)));
}
Ok(vec![first_frame])
} else {
frames.sort_by_key(|path| {
path.file_name()
.and_then(|name| name.to_str())
.and_then(extract_frame_number)
.unwrap_or(0)
});
Ok(frames)
}
}
}
pub fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Some(Commands::Create {
input,
output,
duration,
}) => {
let input_files = find_sequence_frames(&input).map_err(|e| {
CliError::File(format!(
"Failed to find frame sequence in {}: {}",
input.display(),
e
))
})?;
if input_files.is_empty() {
return Err(CliError::Input("No frame files found".to_string()));
}
println!("Found {} frames", input_files.len());
for (i, path) in input_files.iter().enumerate() {
if let Some(name) = path.file_name() {
println!(" Frame {}: {}", i, name.to_string_lossy());
}
}
create_glyph(input_files, output, duration)
}
Some(Commands::Install) => {
println!("Installing Glyph system-wide...");
install::install()?;
println!("Installation complete! Please restart your shell to use the new features.");
Ok(())
}
Some(Commands::Uninstall) => {
println!("Uninstalling Glyph...");
uninstall::uninstall()?;
println!("Uninstallation complete! Please restart your shell.");
Ok(())
}
None => {
if let Some(input) = cli.input {
if input.extension().and_then(|ext| ext.to_str()) == Some("glyph") {
play_glyph(input, cli.loops)
} else {
Err(CliError::Input(
"Input file must have .glyph extension".to_string(),
))
}
} else {
Err(CliError::Input(
"Please provide a .glyph file to play or use a subcommand".to_string(),
))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
fn create_test_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
let path = dir.path().join(name);
let mut file = File::create(&path).unwrap();
writeln!(file, "{}", content).unwrap();
path
}
#[test]
fn test_extract_frame_number() {
assert_eq!(extract_frame_number("frame_001.txt"), Some(1));
assert_eq!(extract_frame_number("frame_1.txt"), Some(1));
assert_eq!(extract_frame_number("frame.001.txt"), Some(1));
assert_eq!(extract_frame_number("frame99.txt"), Some(99));
assert_eq!(extract_frame_number("noframe.txt"), None);
}
#[test]
fn test_find_sequence_simple_numbering() {
let temp_dir = TempDir::new().unwrap();
let frame1 = create_test_file(&temp_dir, "frame_1.txt", "test");
let _frame2 = create_test_file(&temp_dir, "frame_2.txt", "test");
let _frame3 = create_test_file(&temp_dir, "frame_3.txt", "test");
let frames = find_sequence_frames(&frame1).unwrap();
assert_eq!(frames.len(), 3);
assert!(frames.iter().all(|f| f.exists()));
}
#[test]
fn test_find_sequence_padded_numbering() {
let temp_dir = TempDir::new().unwrap();
let frame1 = create_test_file(&temp_dir, "frame_001.txt", "test");
let _frame2 = create_test_file(&temp_dir, "frame_002.txt", "test");
let _frame3 = create_test_file(&temp_dir, "frame_003.txt", "test");
let frames = find_sequence_frames(&frame1).unwrap();
assert_eq!(frames.len(), 3);
let names: Vec<_> = frames
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert_eq!(
names,
vec!["frame_001.txt", "frame_002.txt", "frame_003.txt"]
);
}
#[test]
fn test_find_sequence_mixed_files() {
let temp_dir = TempDir::new().unwrap();
let frame1 = create_test_file(&temp_dir, "frame_001.txt", "test");
create_test_file(&temp_dir, "frame_002.txt", "test");
create_test_file(&temp_dir, "frame_003.txt", "test");
create_test_file(&temp_dir, "other.txt", "test");
create_test_file(&temp_dir, "unrelated_001.txt", "test");
let frames = find_sequence_frames(&frame1).unwrap();
assert_eq!(frames.len(), 3);
for frame in frames {
assert!(frame
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("frame_"));
}
}
#[test]
fn test_find_sequence_directory() {
let temp_dir = TempDir::new().unwrap();
create_test_file(&temp_dir, "frame_001.txt", "test");
create_test_file(&temp_dir, "frame_002.txt", "test");
create_test_file(&temp_dir, "other.txt", "test");
let frames = find_sequence_frames(temp_dir.path()).unwrap();
assert_eq!(frames.len(), 3); }
#[test]
fn test_find_sequence_single_file() {
let temp_dir = TempDir::new().unwrap();
let single_file = create_test_file(&temp_dir, "single.txt", "test");
let frames = find_sequence_frames(&single_file).unwrap();
assert_eq!(frames.len(), 1);
assert_eq!(frames[0], single_file);
}
#[test]
fn test_find_sequence_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let nonexistent = temp_dir.path().join("definitely_nonexistent_file.txt");
let result = find_sequence_frames(&nonexistent);
assert!(result.is_err());
}
}