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:
132
crates/nomina-core/src/rules/move_parts.rs
Normal file
132
crates/nomina-core/src/rules/move_parts.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user