Skip to content

Commit 00f9b4e

Browse files
committed
Add counters to bulk rename function
1 parent 9535caa commit 00f9b4e

File tree

14 files changed

+2352
-31
lines changed

14 files changed

+2352
-31
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: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,44 @@
1-
use std::{borrow::Cow, collections::HashMap, ffi::{OsStr, OsString}, hash::Hash, io::{Read, Write}, ops::Deref};
1+
use std::{
2+
borrow::Cow,
3+
collections::HashMap,
4+
ffi::{OsStr, OsString},
5+
hash::Hash,
6+
io::{Read, Write},
7+
ops::Deref,
8+
};
29

310
use anyhow::{Result, anyhow};
411
use crossterm::{execute, style::Print};
512
use scopeguard::defer;
613
use tokio::io::AsyncWriteExt;
714
use yazi_config::YAZI;
815
use yazi_dds::Pubsub;
9-
use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, paths_to_same_file, services::{self, Local}, skip_url};
16+
use yazi_fs::{
17+
File, FilesOp, max_common_root, maybe_exists, paths_to_same_file,
18+
services::{self, Local},
19+
skip_url,
20+
};
1021
use yazi_macro::{err, succ};
1122
use yazi_parser::VoidOpt;
1223
use yazi_proxy::{AppProxy, HIDER, TasksProxy, WATCHER};
13-
use yazi_shared::{OsStrJoin, event::Data, terminal_clear, url::{Component, Url}};
24+
use yazi_shared::{
25+
OsStrJoin,
26+
event::Data,
27+
terminal_clear,
28+
url::{Component, Url},
29+
};
1430
use yazi_term::tty::TTY;
1531

1632
use crate::{Actor, Ctx};
1733

1834
pub struct BulkRename;
1935

36+
mod counters;
37+
mod filename_template;
38+
mod name_generator;
39+
40+
use name_generator::generate_names;
41+
2042
impl Actor for BulkRename {
2143
type Options = VoidOpt;
2244

@@ -49,31 +71,58 @@ impl Actor for BulkRename {
4971
.await?;
5072

5173
defer! { tokio::spawn(Local::remove_file(tmp.clone())); }
52-
TasksProxy::process_exec(Cow::Borrowed(opener), cwd, vec![
53-
OsString::new(),
54-
tmp.to_owned().into(),
55-
])
74+
TasksProxy::process_exec(
75+
Cow::Borrowed(opener),
76+
cwd,
77+
vec![OsString::new(), tmp.to_owned().into()],
78+
)
5679
.await;
5780

5881
let _permit = HIDER.acquire().await.unwrap();
5982
defer!(AppProxy::resume());
6083
AppProxy::stop().await;
6184

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-
85+
let new_names = Local::read_to_string(&tmp).await?;
86+
let new = Self::parse_new_names(&new_names, old.len()).await?;
7087
Self::r#do(root, old, new, selected).await
7188
});
7289
succ!();
7390
}
7491
}
7592

7693
impl BulkRename {
94+
/// Reads a number of lines from a string, attempting to parse them as either
95+
/// fixed filenames or counter-based templates.
96+
///
97+
/// The number of expected lines must match `expected_count`.
98+
/// If parsing fails, displays all errors to the user and waits for ENTER
99+
/// before returning an error.
100+
async fn parse_new_names(new_names: &str, expected_count: usize) -> Result<Vec<Tuple>> {
101+
match generate_names(&mut new_names.lines().take(expected_count)) {
102+
Ok(paths) => Ok(paths),
103+
Err(errors) => {
104+
let mut buffer = String::new();
105+
errors.iter().for_each(|error| {
106+
let _ = error.write_to(&mut buffer);
107+
});
108+
109+
// Show all parse errors in TTY, then return an error
110+
terminal_clear(TTY.writer())?;
111+
execute!(
112+
TTY.writer(),
113+
Print("Errors encountered while parsing rename lines:\n\n"),
114+
Print(buffer),
115+
Print("\nPress ENTER to exit")
116+
)?;
117+
// Wait for user input
118+
TTY.reader().read_exact(&mut [0])?;
119+
120+
// Return an error to skip further rename
121+
Err(anyhow::anyhow!("Parsing errors in rename lines"))
122+
}
123+
}
124+
}
125+
77126
async fn r#do(root: usize, old: Vec<Tuple>, new: Vec<Tuple>, selected: Vec<Url>) -> Result<()> {
78127
terminal_clear(TTY.writer())?;
79128
if old.len() != new.len() {
@@ -199,25 +248,35 @@ struct Tuple(usize, OsString);
199248
impl Deref for Tuple {
200249
type Target = OsStr;
201250

202-
fn deref(&self) -> &Self::Target { &self.1 }
251+
fn deref(&self) -> &Self::Target {
252+
&self.1
253+
}
203254
}
204255

205256
impl PartialEq for Tuple {
206-
fn eq(&self, other: &Self) -> bool { self.1 == other.1 }
257+
fn eq(&self, other: &Self) -> bool {
258+
self.1 == other.1
259+
}
207260
}
208261

209262
impl Eq for Tuple {}
210263

211264
impl Hash for Tuple {
212-
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.1.hash(state); }
265+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
266+
self.1.hash(state);
267+
}
213268
}
214269

215270
impl AsRef<OsStr> for Tuple {
216-
fn as_ref(&self) -> &OsStr { &self.1 }
271+
fn as_ref(&self) -> &OsStr {
272+
&self.1
273+
}
217274
}
218275

219276
impl Tuple {
220-
fn new(index: usize, inner: impl Into<OsString>) -> Self { Self(index, inner.into()) }
277+
fn new(index: usize, inner: impl Into<OsString>) -> Self {
278+
Self(index, inner.into())
279+
}
221280
}
222281

223282
// --- Tests
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+
}

0 commit comments

Comments
 (0)