diff --git a/src/icon_index.rs b/src/icon_index.rs new file mode 100644 index 0000000..901efab --- /dev/null +++ b/src/icon_index.rs @@ -0,0 +1,89 @@ +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; + +pub fn list_svg_icons(root: &Path) -> Vec { + WalkDir::new(root) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("svg")) + .map(|e| e.path().to_path_buf()) + .collect() +} + +pub fn fuzzy_match(query: &str, candidate: &str) -> bool { + if query.is_empty() { + return true; + } + let candidate_lower = candidate.to_lowercase(); + let mut chars = candidate_lower.chars(); + for q in query.to_lowercase().chars() { + if !chars.any(|c| c == q) { + return false; + } + } + true +} + +pub fn filter_icons<'a>(icons: &'a [PathBuf], query: &str, limit: usize) -> Vec<&'a PathBuf> { + icons + .iter() + .filter(|p| fuzzy_match(query, &p.to_string_lossy())) + .take(limit) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fuzzy_match_empty_query_matches_everything() { + assert!(fuzzy_match("", "anything")); + } + + #[test] + fn fuzzy_match_is_case_insensitive_subsequence() { + assert!(fuzzy_match("cls", "actions/16/edit-clear-symbolic.svg")); + assert!(fuzzy_match("CLS", "actions/16/edit-clear-symbolic.svg")); + } + + #[test] + fn fuzzy_match_rejects_out_of_order_or_missing_chars() { + assert!(!fuzzy_match("zzz", "actions/16/edit-clear-symbolic.svg")); + assert!(!fuzzy_match("scl", "edit-clear-symbolic.svg")); // 's' comes after "cl" here, wrong order + } + + #[test] + fn filter_icons_applies_query_and_limit() { + let icons = vec![ + PathBuf::from("a/edit-clear.svg"), + PathBuf::from("b/edit-copy.svg"), + PathBuf::from("c/folder.svg"), + ]; + + let matches = filter_icons(&icons, "edit", 10); + assert_eq!(matches.len(), 2); + + let limited = filter_icons(&icons, "", 1); + assert_eq!(limited.len(), 1); + } + + #[test] + fn list_svg_icons_finds_only_svg_files_recursively() { + let dir = std::env::temp_dir().join(format!( + "icon-color-tool-test-iconindex-{}", + std::process::id() + )); + let sub = dir.join("actions/16"); + std::fs::create_dir_all(&sub).unwrap(); + std::fs::write(sub.join("edit-clear.svg"), "").unwrap(); + std::fs::write(sub.join("readme.txt"), "not an icon").unwrap(); + + let found = list_svg_icons(&dir); + assert_eq!(found.len(), 1); + assert!(found[0].ends_with("edit-clear.svg")); + + std::fs::remove_dir_all(&dir).ok(); + } +} diff --git a/src/main.rs b/src/main.rs index 87f4dd1..8658643 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod color_scheme; mod config; +mod icon_index; use gtk::prelude::*; use gtk::{glib, Application, ApplicationWindow};