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.
89 lines
2.3 KiB
Rust
89 lines
2.3 KiB
Rust
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();
|
|
}
|
|
}
|