Skip to content

Commit 32e0efc

Browse files
committed
Add counters to bulk rename function
1 parent 9535caa commit 32e0efc

File tree

14 files changed

+2292
-19
lines changed

14 files changed

+2292
-19
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

yazi-actor/Cargo.toml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,18 @@ yazi-term = { path = "../yazi-term", version = "25.6.11" }
2828
yazi-widgets = { path = "../yazi-widgets", version = "25.6.11" }
2929

3030
# External dependencies
31-
anyhow = { workspace = true }
32-
crossterm = { workspace = true }
33-
foldhash = { workspace = true }
34-
futures = { workspace = true }
35-
indexmap = { workspace = true }
36-
mlua = { workspace = true }
37-
paste = { workspace = true }
38-
scopeguard = { workspace = true }
39-
tokio = { workspace = true }
40-
tokio-stream = { workspace = true }
41-
tracing = { workspace = true }
31+
anyhow = { workspace = true }
32+
crossterm = { workspace = true }
33+
foldhash = { workspace = true }
34+
futures = { workspace = true }
35+
indexmap = { workspace = true }
36+
mlua = { workspace = true }
37+
paste = { workspace = true }
38+
scopeguard = { workspace = true }
39+
tokio = { workspace = true }
40+
tokio-stream = { workspace = true }
41+
tracing = { workspace = true }
42+
unicode-width = { workspace = true }
4243

4344
[target."cfg(unix)".dependencies]
4445
libc = { workspace = true }

yazi-actor/src/mgr/bulk_rename.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ use crate::{Actor, Ctx};
1717

1818
pub struct BulkRename;
1919

20+
mod counters;
21+
mod filename_template;
22+
mod name_generator;
23+
24+
use name_generator::generate_names;
25+
2026
impl Actor for BulkRename {
2127
type Options = VoidOpt;
2228

@@ -59,21 +65,38 @@ impl Actor for BulkRename {
5965
defer!(AppProxy::resume());
6066
AppProxy::stop().await;
6167

62-
let new: Vec<_> = Local::read_to_string(&tmp)
63-
.await?
64-
.lines()
65-
.take(old.len())
66-
.enumerate()
67-
.map(|(i, s)| Tuple::new(i, s))
68-
.collect();
69-
68+
let new_names = Local::read_to_string(&tmp).await?;
69+
let new = Self::parse_new_names(&new_names, old.len()).await?;
7070
Self::r#do(root, old, new, selected).await
7171
});
7272
succ!();
7373
}
7474
}
7575

7676
impl BulkRename {
77+
/// Reads a number of lines from a string, attempting to parse them as either
78+
/// fixed filenames or counter-based templates.
79+
///
80+
/// The number of expected lines should match `expected_count`.
81+
/// If parsing fails, displays all errors to the user and waits for ENTER
82+
/// before returning an error.
83+
async fn parse_new_names(new_names: &str, expected_count: usize) -> Result<Vec<Tuple>> {
84+
match generate_names(&mut new_names.lines().take(expected_count)) {
85+
Ok(paths) => Ok(paths),
86+
Err(errors) => {
87+
// Show all parse errors in TTY, then return an error
88+
terminal_clear(TTY.writer())?;
89+
let err = format! {"Found errors in the filenames:\n\n{errors}\nPress ENTER to exit"};
90+
execute!(TTY.writer(), Print(err),)?;
91+
// Wait for user input
92+
TTY.reader().read_exact(&mut [0])?;
93+
94+
// Return an error to skip further rename
95+
Err(anyhow::anyhow!("Parsing errors in rename lines"))
96+
}
97+
}
98+
}
99+
77100
async fn r#do(root: usize, old: Vec<Tuple>, new: Vec<Tuple>, selected: Vec<Url>) -> Result<()> {
78101
terminal_clear(TTY.writer())?;
79102
if old.len() != new.len() {
Lines changed: 64 additions & 0 deletions
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+
}
Lines changed: 104 additions & 0 deletions
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+
}
Lines changed: 33 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)