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,132 @@
use serde::{Deserialize, Serialize};
use crate::{RenameContext, RenameRule};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum MoveTarget {
None,
Position(usize),
Start,
End,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MovePartsRule {
pub source_from: usize,
pub source_length: usize,
pub target: MoveTarget,
pub separator: String,
pub copy_mode: bool,
pub enabled: bool,
}
impl MovePartsRule {
pub fn new() -> Self {
Self {
source_from: 0,
source_length: 0,
target: MoveTarget::None,
separator: String::new(),
copy_mode: false,
enabled: true,
}
}
}
impl RenameRule for MovePartsRule {
fn apply(&self, filename: &str, _context: &RenameContext) -> String {
if self.target == MoveTarget::None || self.source_length == 0 {
return filename.to_string();
}
let chars: Vec<char> = filename.chars().collect();
if self.source_from >= chars.len() {
return filename.to_string();
}
let end = (self.source_from + self.source_length).min(chars.len());
let extracted: String = chars[self.source_from..end].iter().collect();
let remaining: String = if self.copy_mode {
filename.to_string()
} else {
let mut r = String::new();
for (i, c) in chars.iter().enumerate() {
if i < self.source_from || i >= end {
r.push(*c);
}
}
r
};
match &self.target {
MoveTarget::None => filename.to_string(),
MoveTarget::Start => {
if self.separator.is_empty() {
format!("{}{}", extracted, remaining)
} else {
format!("{}{}{}", extracted, self.separator, remaining)
}
}
MoveTarget::End => {
if self.separator.is_empty() {
format!("{}{}", remaining, extracted)
} else {
format!("{}{}{}", remaining, self.separator, extracted)
}
}
MoveTarget::Position(pos) => {
let rem_chars: Vec<char> = remaining.chars().collect();
let p = (*pos).min(rem_chars.len());
let before: String = rem_chars[..p].iter().collect();
let after: String = rem_chars[p..].iter().collect();
format!("{}{}{}", before, extracted, after)
}
}
}
fn display_name(&self) -> &str {
"Move/Copy"
}
fn rule_type(&self) -> &str {
"move_parts"
}
fn is_enabled(&self) -> bool {
self.enabled
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn move_to_end() {
let rule = MovePartsRule {
source_from: 0,
source_length: 3,
target: MoveTarget::End,
separator: "_".into(),
copy_mode: false,
..MovePartsRule::new()
};
let ctx = RenameContext::dummy(0);
assert_eq!(rule.apply("abcdef", &ctx), "def_abc");
}
#[test]
fn copy_to_start() {
let rule = MovePartsRule {
source_from: 3,
source_length: 3,
target: MoveTarget::Start,
separator: "-".into(),
copy_mode: true,
..MovePartsRule::new()
};
let ctx = RenameContext::dummy(0);
assert_eq!(rule.apply("abcdef", &ctx), "def-abcdef");
}
}