feat: add icon_index module for recursive SVG listing and fuzzy search
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
pub fn list_svg_icons(root: &Path) -> Vec<PathBuf> {
|
||||||
|
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"), "<svg></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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
mod color_scheme;
|
mod color_scheme;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod icon_index;
|
||||||
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{glib, Application, ApplicationWindow};
|
use gtk::{glib, Application, ApplicationWindow};
|
||||||
|
|||||||
Reference in New Issue
Block a user