Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 11 additions & 10 deletions yazi-actor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,17 @@ yazi-watcher = { path = "../yazi-watcher", version = "25.9.15" }
yazi-widgets = { path = "../yazi-widgets", version = "25.9.15" }

# External dependencies
anyhow = { workspace = true }
crossterm = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true }
mlua = { workspace = true }
paste = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
anyhow = { workspace = true }
crossterm = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true }
mlua = { workspace = true }
paste = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
unicode-width = { workspace = true }

[target."cfg(unix)".dependencies]
libc = { workspace = true }
Expand Down
40 changes: 31 additions & 9 deletions yazi-actor/src/mgr/bulk_rename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ use crate::{Actor, Ctx};

pub struct BulkRename;

mod counters;
mod filename_template;
mod name_generator;

use name_generator::generate_names;

impl Actor for BulkRename {
type Options = VoidOpt;

Expand Down Expand Up @@ -60,22 +66,38 @@ impl Actor for BulkRename {
defer!(AppProxy::resume());
AppProxy::stop().await;

let new: Vec<_> = Local
.read_to_string(&tmp)
.await?
.lines()
.take(old.len())
.enumerate()
.map(|(i, s)| Tuple::new(i, s))
.collect();

let new_names = Local.read_to_string(&tmp).await?;
let new = Self::parse_new_names(&new_names, old.len()).await?;
Self::r#do(root, old, new, selected).await
});
succ!();
}
}

impl BulkRename {
/// Reads a number of lines from a string, attempting to parse them as either
/// fixed filenames or counter-based templates.
///
/// The number of expected lines should match `expected_count`.
/// If parsing fails, displays all errors to the user and waits for ENTER
/// before returning an error.
async fn parse_new_names(new_names: &str, expected_count: usize) -> Result<Vec<Tuple>> {
match generate_names(&mut new_names.lines().take(expected_count)) {
Ok(paths) => Ok(paths),
Err(errors) => {
// Show all parse errors in TTY, then return an error
terminal_clear(TTY.writer())?;
let err = format! {"Found errors in the filenames:\n\n{errors}\nPress ENTER to exit"};
execute!(TTY.writer(), Print(err),)?;
// Wait for user input
TTY.reader().read_exact(&mut [0])?;

// Return an error to skip further rename
Err(anyhow::anyhow!("Parsing errors in rename lines"))
}
}
}

async fn r#do(
root: usize,
old: Vec<Tuple>,
Expand Down
64 changes: 64 additions & 0 deletions yazi-actor/src/mgr/bulk_rename/counters/ansi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! This module provides functionality for managing ANSI letter counters for both
//! uppercase and lowercase letters, following Excel's alphabetic counter style.

use super::{CounterFormatter, LOWERCASE, UPPERCASE, write_number_as_letters_gen};
use std::fmt;

/// A helper structure for generating uppercase ANSI letters (e.g., A, B, ..., AA, AB).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnsiUpper;

/// A helper structure for generating lowercase ANSI letters (e.g., a, b, ..., aa, ab).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnsiLower;

impl_counter_formatter! { AnsiUpper, UPPERCASE }
impl_counter_formatter! { AnsiLower, LOWERCASE }

/// Converts ANSI letters (e.g., "A", "Z", "AA") to their corresponding numeric values.
/// The conversion follows Excel's alphabetic counter rules: 'A' = 1, 'B' = 2, ...,
/// 'Z' = 26, 'AA' = 27, etc.
///
/// The `UPPERCASE` constant determines whether the string should be validated
/// as uppercase or lowercase.
///
/// # Returns
///
/// Returns `Some(u32)` if conversion is successful; otherwise, returns `None`.
#[inline]
fn convert_letters_to_number<const UPPERCASE: bool>(value: &str) -> Option<u32> {
if value.is_empty() {
return None;
}

if UPPERCASE {
if !value.chars().all(|c| c.is_ascii_uppercase()) {
return None;
}
} else if !value.chars().all(|c| c.is_ascii_lowercase()) {
return None;
}

let result = value.chars().rev().enumerate().fold(0_u32, |acc, (i, c)| {
acc + ((c as u32) - (if UPPERCASE { 'A' } else { 'a' } as u32) + 1) * 26_u32.pow(i as u32)
});

Some(result)
}

/// Writes the numeric value as ANSI letters (e.g., 1 → "A", 27 → "AA") into the provided buffer.
///
/// # Arguments
///
/// * `num` - The numeric value to convert.
/// * `width` - The minimum width of the generated string, padded with zeros if necessary.
/// * `buf` - The buffer to write the resulting string into.
#[inline]
fn write_number_as_letters<const UPPERCASE: bool>(
num: u32,
width: usize,
buf: &mut impl fmt::Write,
) -> fmt::Result {
let base = if UPPERCASE { b'A' } else { b'a' };
write_number_as_letters_gen(num, width, 26, |r| (base + r as u8) as char, buf)
}
104 changes: 104 additions & 0 deletions yazi-actor/src/mgr/bulk_rename/counters/cyrillic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! This module provides functionality for managing Cyrillic letter counters for both
//! uppercase and lowercase letters, following Excel's alphabetic counter style.

use super::{CounterFormatter, LOWERCASE, UPPERCASE, write_number_as_letters_gen};
use std::fmt;

/// An array of uppercase Cyrillic letters used for indexing and mapping.
/// This array includes all uppercase Cyrillic letters excluding 'Ё', 'Й', 'Ъ', 'Ы', 'Ь'.
const UPPERCASE_CYRILLIC: [char; 28] = [
'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ж', 'З', 'И', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У',
'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Э', 'Ю', 'Я',
];

/// An array of lowercase Cyrillic letters used for indexing and mapping.
/// This array includes all lowercase Cyrillic letters excluding 'ё', 'й', 'ъ', 'ы', 'ь'.
const LOWERCASE_CYRILLIC: [char; 28] = [
'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у',
'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'э', 'ю', 'я',
];

/// A helper structure for generating uppercase Cyrillic letters (e.g., А, Б, В, ..., АА, АБ),
/// while excluding 'Ё', 'Й', 'Ъ', 'Ы' and 'Ь'.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CyrillicUpper;

/// A helper structure for generating lowercase Cyrillic letters (e.g., а, б, в, ..., аа, аб),
/// while excluding 'ё', 'й', 'ъ', 'ы' and 'ь'.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CyrillicLower;

impl_counter_formatter! { CyrillicUpper, UPPERCASE }
impl_counter_formatter! { CyrillicLower, LOWERCASE }

/// Converts Cyrillic letters (e.g., "Б", "В", "БА") to their corresponding numeric values.
/// The conversion follows Excel's alphabetic counter rules: 'А' = 1, 'Б' = 2, ...,
/// 'Я' = 28, 'АА' = 29, etc.
///
/// The `UPPERCASE` constant determines whether the string should be validated
/// as uppercase or lowercase.
///
/// # Returns
///
/// Returns `Some(u32)` if conversion is successful; otherwise, returns `None`.
#[inline]
fn convert_letters_to_number<const UPPERCASE: bool>(value: &str) -> Option<u32> {
if invalid_string::<UPPERCASE>(value) {
return None;
}
let lookup = if UPPERCASE { &UPPERCASE_CYRILLIC } else { &LOWERCASE_CYRILLIC };

let result = value.chars().rev().enumerate().fold(0_u32, |acc, (i, c)| {
if let Some(index) = lookup.iter().position(|&x| x == c) {
acc + (index as u32 + 1) * 28_u32.pow(i as u32)
} else {
acc
}
});
Some(result)
}

/// Writes the numeric value as Cyrillic letters (e.g., 1 → "А", 28 → "Я") into the provided buffer.
///
/// # Arguments
///
/// * `num` - The numeric value to convert.
/// * `width` - The minimum width of the generated string, padded with zeros if necessary.
/// * `buf` - The buffer to write the resulting string into.
#[inline]
fn write_number_as_letters<const UPPERCASE: bool>(
num: u32,
width: usize,
buf: &mut impl fmt::Write,
) -> fmt::Result {
let lookup = if UPPERCASE { &UPPERCASE_CYRILLIC } else { &LOWERCASE_CYRILLIC };

write_number_as_letters_gen(num, width, 28, |remainder| lookup[remainder as usize], buf)
}

/// Checks if a string is non-empty and consists only of valid uppercase or
/// lowercase Cyrillic letters, excluding 'Ё', 'Й', 'Ъ', 'Ы', and 'Ь'
/// ('ё', 'й', 'ъ', 'ы' and 'ь').
///
/// The `UPPERCASE` constant determines whether to check uppercase or lowercase letters.
///
/// # Returns
///
/// Returns `true` if the string is invalid; otherwise, returns `false`.
#[inline]
fn invalid_string<const UPPERCASE: bool>(str: &str) -> bool {
if str.is_empty() {
return true;
}
if UPPERCASE {
!str.chars().all(|c| {
// ('А'..='Я') == ('\u{0410}'..='\u{042F}')
('\u{0410}'..='\u{042F}').contains(&c) && !matches!(c, 'Ё' | 'Й' | 'Ъ' | 'Ы' | 'Ь')
})
} else {
!str.chars().all(|c| {
// ('а'..='я') == ('\u{0430}'..='\u{044F}')
('\u{0430}'..='\u{044F}').contains(&c) && !matches!(c, 'ё' | 'й' | 'ъ' | 'ы' | 'ь')
})
}
}
33 changes: 33 additions & 0 deletions yazi-actor/src/mgr/bulk_rename/counters/digit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! This module provides functionality for managing Arabic numeral counters.

use super::CounterFormatter;
use std::fmt;

/// A helper structure for generating numeric values (e.g., 1, 2, ..., 999 or 001, 002).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Digits;

impl CounterFormatter for Digits {
/// Formats a value as a zero-padded string and writes it to a buffer.
///
/// # Arguments
///
/// * `value` - The numeric value to format.
/// * `width` - The minimum width of the output string.
/// * `buf` - A mutable reference to a buffer.
#[inline]
fn value_to_buffer(
self,
value: u32,
width: usize,
buf: &mut impl fmt::Write,
) -> Result<(), fmt::Error> {
write!(buf, "{value:0>width$}")
}

/// Parses a zero-padded numeric string into a `u32` value.
#[inline]
fn string_to_value(self, value: &str) -> Option<u32> {
value.parse().ok()
}
}
Loading
Loading