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,88 @@
use std::path::PathBuf;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::NominaError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UndoLog {
pub entries: Vec<UndoBatch>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UndoBatch {
pub id: Uuid,
pub timestamp: DateTime<Utc>,
pub description: String,
pub operations: Vec<UndoEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UndoEntry {
pub original_path: PathBuf,
pub renamed_path: PathBuf,
}
const MAX_UNDO_BATCHES: usize = 50;
impl UndoLog {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn load(path: &std::path::Path) -> crate::Result<Self> {
if !path.exists() {
return Ok(Self::new());
}
let data = std::fs::read_to_string(path).map_err(|e| NominaError::Filesystem {
path: path.to_path_buf(),
source: e,
})?;
serde_json::from_str(&data).map_err(|e| NominaError::PresetError {
reason: e.to_string(),
})
}
pub fn save(&self, path: &std::path::Path) -> crate::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| NominaError::Filesystem {
path: parent.to_path_buf(),
source: e,
})?;
}
let json = serde_json::to_string_pretty(self).map_err(|e| NominaError::PresetError {
reason: e.to_string(),
})?;
std::fs::write(path, json).map_err(|e| NominaError::Filesystem {
path: path.to_path_buf(),
source: e,
})
}
pub fn add_batch(&mut self, batch: UndoBatch) {
self.entries.push(batch);
while self.entries.len() > MAX_UNDO_BATCHES {
self.entries.remove(0);
}
}
pub fn undo_last(&mut self) -> Option<UndoBatch> {
self.entries.pop()
}
pub fn undo_by_id(&mut self, id: Uuid) -> Option<UndoBatch> {
if let Some(pos) = self.entries.iter().position(|b| b.id == id) {
Some(self.entries.remove(pos))
} else {
None
}
}
pub fn clear(&mut self) {
self.entries.clear();
}
}