Skip to content

Commit fa119d3

Browse files
committed
Add counters to bulk rename function
1 parent 83f4dee commit fa119d3

File tree

12 files changed

+2364
-1
lines changed

12 files changed

+2364
-1
lines changed

yazi-core/src/mgr/commands/bulk_rename.rs

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, io::{Read, Write}, path::PathBuf};
22

33
use anyhow::{Result, anyhow};
4+
use crossterm::terminal;
45
use scopeguard::defer;
56
use tokio::{fs::{self, OpenOptions}, io::AsyncWriteExt};
67
use yazi_config::YAZI;
@@ -11,6 +12,12 @@ use yazi_shared::{terminal_clear, tty::TTY, url::Url};
1112

1213
use crate::mgr::Mgr;
1314

15+
mod counters;
16+
mod filename_template;
17+
mod name_generator;
18+
19+
use name_generator::generate_names;
20+
1421
impl Mgr {
1522
pub(super) fn bulk_rename(&self) {
1623
let Some(opener) = YAZI.opener.block(YAZI.open.all("bulk-rename.txt", "text/plain")) else {
@@ -45,11 +52,45 @@ impl Mgr {
4552
defer!(AppProxy::resume());
4653
AppProxy::stop().await;
4754

48-
let new: Vec<_> = fs::read_to_string(&tmp).await?.lines().map(PathBuf::from).collect();
55+
let new_names = fs::read_to_string(&tmp).await?;
56+
let new = Self::parse_new_names(&new_names).await?;
4957
Self::bulk_rename_do(root, old, new).await
5058
});
5159
}
5260

61+
/// Reads lines from a string, tries to parse them as either fixed filenames or
62+
/// counter-based templates (via `generate_names`).
63+
///
64+
/// If parsing fails, this function will display the errors to the user and wait
65+
/// for an ENTER key press before returning an error.
66+
async fn parse_new_names(new_names: &str) -> Result<Vec<PathBuf>> {
67+
match generate_names(new_names) {
68+
Ok(paths) => Ok(paths),
69+
Err(errors) => {
70+
let (width, _) = terminal::size().unwrap_or((80, 120));
71+
let mut buffer = String::new();
72+
errors.iter().for_each(|error| {
73+
let _ = error.write_to(&mut buffer, width);
74+
});
75+
76+
// Show all parse errors in TTY, then return an error
77+
terminal_clear(TTY.writer())?;
78+
{
79+
let mut w = TTY.lockout();
80+
writeln!(w, "Errors encountered while parsing rename lines:")?;
81+
writeln!(w, "{buffer}")?;
82+
writeln!(w, "\nPress ENTER to exit")?;
83+
w.flush()?;
84+
}
85+
// Wait for user input
86+
TTY.reader().read_exact(&mut [0])?;
87+
88+
// Return an error to skip further rename
89+
Err(anyhow::anyhow!("Parsing errors in rename lines"))
90+
}
91+
}
92+
}
93+
5394
async fn bulk_rename_do(root: PathBuf, old: Vec<PathBuf>, new: Vec<PathBuf>) -> Result<()> {
5495
terminal_clear(TTY.writer())?;
5596
if old.len() != new.len() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! This module provides functionality for managing ANSI letter counters for both
2+
//! uppercase and lowercase letters, following Excel's alphabetic counter style.
3+
4+
use super::{CounterFormatter, LOWERCASE, UPPERCASE, write_number_as_letters_gen};
5+
use std::fmt;
6+
7+
/// A helper structure for generating uppercase ANSI letters (e.g., A, B, ..., AA, AB).
8+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9+
pub struct AnsiUpper;
10+
11+
/// A helper structure for generating lowercase ANSI letters (e.g., a, b, ..., aa, ab).
12+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13+
pub struct AnsiLower;
14+
15+
impl_counter_formatter! { AnsiUpper, UPPERCASE }
16+
impl_counter_formatter! { AnsiLower, LOWERCASE }
17+
18+
/// Converts ANSI letters (e.g., "A", "Z", "AA") to their corresponding numeric values.
19+
/// The conversion follows Excel's alphabetic counter rules: 'A' = 1, 'B' = 2, ...,
20+
/// 'Z' = 26, 'AA' = 27, etc.
21+
///
22+
/// The `UPPERCASE` constant determines whether the string should be validated
23+
/// as uppercase or lowercase.
24+
///
25+
/// # Returns
26+
///
27+
/// Returns `Some(u32)` if conversion is successful; otherwise, returns `None`.
28+
#[inline]
29+
fn convert_letters_to_number<const UPPERCASE: bool>(value: &str) -> Option<u32> {
30+
if value.is_empty() {
31+
return None;
32+
}
33+
34+
if UPPERCASE {
35+
if !value.chars().all(|c| c.is_ascii_uppercase()) {
36+
return None;
37+
}
38+
} else if !value.chars().all(|c| c.is_ascii_lowercase()) {
39+
return None;
40+
}
41+
42+
let result = value.chars().rev().enumerate().fold(0_u32, |acc, (i, c)| {
43+
acc + ((c as u32) - (if UPPERCASE { 'A' } else { 'a' } as u32) + 1) * 26_u32.pow(i as u32)
44+
});
45+
46+
Some(result)
47+
}
48+
49+
/// Writes the numeric value as ANSI letters (e.g., 1 → "A", 27 → "AA") into the provided buffer.
50+
///
51+
/// # Arguments
52+
///
53+
/// * `num` - The numeric value to convert.
54+
/// * `width` - The minimum width of the generated string, padded with zeros if necessary.
55+
/// * `buf` - The buffer to write the resulting string into.
56+
#[inline]
57+
fn write_number_as_letters<const UPPERCASE: bool>(
58+
num: u32,
59+
width: usize,
60+
buf: &mut impl fmt::Write,
61+
) -> fmt::Result {
62+
let base = if UPPERCASE { b'A' } else { b'a' };
63+
write_number_as_letters_gen(num, width, 26, |r| (base + r as u8) as char, buf)
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//! This module provides functionality for managing Cyrillic letter counters for both
2+
//! uppercase and lowercase letters, following Excel's alphabetic counter style.
3+
4+
use super::{CounterFormatter, LOWERCASE, UPPERCASE, write_number_as_letters_gen};
5+
use std::fmt;
6+
7+
/// An array of uppercase Cyrillic letters used for indexing and mapping.
8+
/// This array includes all uppercase Cyrillic letters excluding 'Ё', 'Й', 'Ъ', 'Ы', 'Ь'.
9+
const UPPERCASE_CYRILLIC: [char; 28] = [
10+
'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ж', 'З', 'И', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У',
11+
'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Э', 'Ю', 'Я',
12+
];
13+
14+
/// An array of lowercase Cyrillic letters used for indexing and mapping.
15+
/// This array includes all lowercase Cyrillic letters excluding 'ё', 'й', 'ъ', 'ы', 'ь'.
16+
const LOWERCASE_CYRILLIC: [char; 28] = [
17+
'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у',
18+
'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'э', 'ю', 'я',
19+
];
20+
21+
/// A helper structure for generating uppercase Cyrillic letters (e.g., А, Б, В, ..., АА, АБ),
22+
/// while excluding 'Ё', 'Й', 'Ъ', 'Ы' and 'Ь'.
23+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24+
pub struct CyrillicUpper;
25+
26+
/// A helper structure for generating lowercase Cyrillic letters (e.g., а, б, в, ..., аа, аб),
27+
/// while excluding 'ё', 'й', 'ъ', 'ы' and 'ь'.
28+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29+
pub struct CyrillicLower;
30+
31+
impl_counter_formatter! { CyrillicUpper, UPPERCASE }
32+
impl_counter_formatter! { CyrillicLower, LOWERCASE }
33+
34+
/// Converts Cyrillic letters (e.g., "Б", "В", "БА") to their corresponding numeric values.
35+
/// The conversion follows Excel's alphabetic counter rules: 'А' = 1, 'Б' = 2, ...,
36+
/// 'Я' = 28, 'АА' = 29, etc.
37+
///
38+
/// The `UPPERCASE` constant determines whether the string should be validated
39+
/// as uppercase or lowercase.
40+
///
41+
/// # Returns
42+
///
43+
/// Returns `Some(u32)` if conversion is successful; otherwise, returns `None`.
44+
#[inline]
45+
fn convert_letters_to_number<const UPPERCASE: bool>(value: &str) -> Option<u32> {
46+
if invalid_string::<UPPERCASE>(value) {
47+
return None;
48+
}
49+
let lookup = if UPPERCASE { &UPPERCASE_CYRILLIC } else { &LOWERCASE_CYRILLIC };
50+
51+
let result = value.chars().rev().enumerate().fold(0_u32, |acc, (i, c)| {
52+
if let Some(index) = lookup.iter().position(|&x| x == c) {
53+
acc + (index as u32 + 1) * 28_u32.pow(i as u32)
54+
} else {
55+
acc
56+
}
57+
});
58+
Some(result)
59+
}
60+
61+
/// Writes the numeric value as Cyrillic letters (e.g., 1 → "А", 28 → "Я") into the provided buffer.
62+
///
63+
/// # Arguments
64+
///
65+
/// * `num` - The numeric value to convert.
66+
/// * `width` - The minimum width of the generated string, padded with zeros if necessary.
67+
/// * `buf` - The buffer to write the resulting string into.
68+
#[inline]
69+
fn write_number_as_letters<const UPPERCASE: bool>(
70+
num: u32,
71+
width: usize,
72+
buf: &mut impl fmt::Write,
73+
) -> fmt::Result {
74+
let lookup = if UPPERCASE { &UPPERCASE_CYRILLIC } else { &LOWERCASE_CYRILLIC };
75+
76+
write_number_as_letters_gen(num, width, 28, |remainder| lookup[remainder as usize], buf)
77+
}
78+
79+
/// Checks if a string is non-empty and consists only of valid uppercase or
80+
/// lowercase Cyrillic letters, excluding 'Ё', 'Й', 'Ъ', 'Ы', and 'Ь'
81+
/// ('ё', 'й', 'ъ', 'ы' and 'ь').
82+
///
83+
/// The `UPPERCASE` constant determines whether to check uppercase or lowercase letters.
84+
///
85+
/// # Returns
86+
///
87+
/// Returns `true` if the string is invalid; otherwise, returns `false`.
88+
#[inline]
89+
fn invalid_string<const UPPERCASE: bool>(str: &str) -> bool {
90+
if str.is_empty() {
91+
return true;
92+
}
93+
if UPPERCASE {
94+
!str.chars().all(|c| {
95+
// ('А'..='Я') == ('\u{0410}'..='\u{042F}')
96+
('\u{0410}'..='\u{042F}').contains(&c) && !matches!(c, 'Ё' | 'Й' | 'Ъ' | 'Ы' | 'Ь')
97+
})
98+
} else {
99+
!str.chars().all(|c| {
100+
// ('а'..='я') == ('\u{0430}'..='\u{044F}')
101+
('\u{0430}'..='\u{044F}').contains(&c) && !matches!(c, 'ё' | 'й' | 'ъ' | 'ы' | 'ь')
102+
})
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//! This module provides functionality for managing Arabic numeral counters.
2+
3+
use super::CounterFormatter;
4+
use std::fmt;
5+
6+
/// A helper structure for generating numeric values (e.g., 1, 2, ..., 999 or 001, 002).
7+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8+
pub struct Digits;
9+
10+
impl CounterFormatter for Digits {
11+
/// Formats a value as a zero-padded string and writes it to a buffer.
12+
///
13+
/// # Arguments
14+
///
15+
/// * `value` - The numeric value to format.
16+
/// * `width` - The minimum width of the output string.
17+
/// * `buf` - A mutable reference to a buffer.
18+
#[inline]
19+
fn value_to_buffer(
20+
self,
21+
value: u32,
22+
width: usize,
23+
buf: &mut impl fmt::Write,
24+
) -> Result<(), fmt::Error> {
25+
write!(buf, "{value:0>width$}")
26+
}
27+
28+
/// Parses a zero-padded numeric string into a `u32` value.
29+
#[inline]
30+
fn string_to_value(self, value: &str) -> Option<u32> {
31+
value.parse().ok()
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use std::fmt;
2+
3+
/// This macro generates an implementation of CounterFormatter for a given
4+
/// counter helper type.
5+
///
6+
/// # Arguments
7+
///
8+
/// * `$type` - The target helper struct (e.g., `AnsiUpper`).
9+
/// * `$case` - A boolean constant determining whether the counter uppercase or lowercase.
10+
macro_rules! impl_counter_formatter {
11+
($type:ty, $case:expr) => {
12+
impl CounterFormatter for $type {
13+
#[inline]
14+
fn value_to_buffer(self, value: u32, width: usize, buf: &mut impl fmt::Write) -> fmt::Result {
15+
write_number_as_letters::<{ $case }>(value, width, buf)
16+
}
17+
18+
#[inline]
19+
fn string_to_value(self, value: &str) -> Option<u32> {
20+
convert_letters_to_number::<{ $case }>(value)
21+
}
22+
}
23+
};
24+
}
25+
26+
/// Converts a given numeric value into an alphabetic representation following a base-N numbering system,
27+
/// similar to Excel-style column labels (e.g., 1 → A, 2 → B, ..., 26 → Z, 27 → AA, etc.).
28+
///
29+
/// This function generalizes the process for different alphabets by allowing a customizable base (`alphabet_len`)
30+
/// and a transformation function (`convert_fn`) that maps remainder values to characters.
31+
///
32+
/// # Arguments
33+
///
34+
/// * `num` - The numeric value to be converted. Since alphabetic numbering systems start from 1
35+
/// (e.g., A = 1, B = 2), it should be non-zero value.
36+
///
37+
/// * `width` - The minimum width of the output string. If necessary, the result will be left-padded with '0'.
38+
///
39+
/// * `alphabet_len` - The base of the numbering system (e.g., 26 for Latin, 28 for Cyrillic, etc.).
40+
///
41+
/// * `convert_fn` - A closure that converts a remainder (`u32`) into a corresponding character.
42+
/// - The `remainder` represents the remainder of division by `alphabet_len` (i.e., `num % alphabet_len`).
43+
/// - The closure should map this remainder to a specific character in the corresponding alphabet
44+
/// (e.g., `b'A' + remainder as u8`).
45+
///
46+
/// * `buf` - A mutable reference to a `fmt::Write` buffer where the result is written.
47+
#[inline]
48+
pub(super) fn write_number_as_letters_gen(
49+
mut num: u32,
50+
width: usize,
51+
alphabet_len: u32,
52+
mut convert_fn: impl FnMut(u32) -> char,
53+
buf: &mut impl fmt::Write,
54+
) -> fmt::Result {
55+
if num == 0 {
56+
return Ok(());
57+
}
58+
59+
let mut stack_buf = ['0'; 10];
60+
let mut written_len = 0;
61+
62+
for char in &mut stack_buf {
63+
if num == 0 {
64+
break;
65+
}
66+
let remainder = (num - 1) % alphabet_len;
67+
*char = convert_fn(remainder);
68+
num = (num - remainder - 1) / alphabet_len;
69+
written_len += 1;
70+
}
71+
72+
if num > 0 {
73+
let mut vec_buf = Vec::with_capacity(20);
74+
vec_buf.extend_from_slice(&stack_buf);
75+
76+
while num > 0 {
77+
let remainder = (num - 1) % alphabet_len;
78+
vec_buf.push(convert_fn(remainder));
79+
num = (num - remainder - 1) / alphabet_len;
80+
written_len += 1;
81+
}
82+
83+
for _ in vec_buf.len()..width {
84+
buf.write_char('0')?;
85+
}
86+
for &c in vec_buf.iter().rev() {
87+
buf.write_char(c)?;
88+
}
89+
} else {
90+
for _ in written_len..width {
91+
buf.write_char('0')?;
92+
}
93+
for &c in stack_buf[..written_len].iter().rev() {
94+
buf.write_char(c)?;
95+
}
96+
}
97+
98+
Ok(())
99+
}

0 commit comments

Comments
 (0)