initial project scaffold

Rust workspace with nomina-core (rename engine) and nomina-app (Tauri v2 shell).
React/TypeScript frontend with tabbed rule panels, virtual-scrolled file list,
and Zustand state management. All 9 rule types implemented with 25 passing tests.
This commit is contained in:
2026-03-13 23:49:29 +02:00
commit 9dca2bedfa
69 changed files with 17462 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use walkdir::WalkDir;
use crate::filter::FilterConfig;
use crate::FileEntry;
pub struct FileScanner {
pub root: PathBuf,
pub filters: FilterConfig,
}
impl FileScanner {
pub fn new(root: PathBuf, filters: FilterConfig) -> Self {
Self { root, filters }
}
pub fn scan(&self) -> Vec<FileEntry> {
let max_depth = self.filters.subfolder_depth.map(|d| d + 1).unwrap_or(usize::MAX);
let walker = WalkDir::new(&self.root)
.max_depth(max_depth)
.follow_links(false);
let mut entries: Vec<FileEntry> = Vec::new();
for result in walker {
let dir_entry = match result {
Ok(e) => e,
Err(_) => continue,
};
// skip the root directory itself
if dir_entry.path() == self.root {
continue;
}
let metadata = match dir_entry.metadata() {
Ok(m) => m,
Err(_) => continue,
};
let path = dir_entry.path().to_path_buf();
let name = dir_entry.file_name().to_string_lossy().to_string();
let is_dir = metadata.is_dir();
let is_hidden = is_hidden_file(&path);
let (stem, extension) = if is_dir {
(name.clone(), String::new())
} else {
split_filename(&name)
};
let created = file_created(&metadata);
let modified = file_modified(&metadata);
let accessed = file_accessed(&metadata);
let entry = FileEntry {
path,
name,
stem,
extension,
size: metadata.len(),
is_dir,
is_hidden,
created,
modified,
accessed,
};
if self.filters.matches(&entry) {
entries.push(entry);
}
}
// natural sort
entries.sort_by(|a, b| natord::compare(&a.name, &b.name));
entries
}
}
fn split_filename(name: &str) -> (String, String) {
match name.rsplit_once('.') {
Some((stem, ext)) if !stem.is_empty() => (stem.to_string(), ext.to_string()),
_ => (name.to_string(), String::new()),
}
}
fn is_hidden_file(path: &Path) -> bool {
#[cfg(target_os = "windows")]
{
use std::os::windows::fs::MetadataExt;
if let Ok(meta) = std::fs::metadata(path) {
const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2;
return meta.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0;
}
}
path.file_name()
.map(|n| n.to_string_lossy().starts_with('.'))
.unwrap_or(false)
}
fn file_created(meta: &std::fs::Metadata) -> Option<DateTime<Utc>> {
meta.created().ok().map(|t| DateTime::<Utc>::from(t))
}
fn file_modified(meta: &std::fs::Metadata) -> Option<DateTime<Utc>> {
meta.modified().ok().map(|t| DateTime::<Utc>::from(t))
}
fn file_accessed(meta: &std::fs::Metadata) -> Option<DateTime<Utc>> {
meta.accessed().ok().map(|t| DateTime::<Utc>::from(t))
}