Skip to content

Allow cycle rename during bulk renaming #4034

Description

@likelikeslike

ya env output

Yazi
    Version  : 26.5.6 (Homebrew 2026-05-05)
    Debug    : false
    Triple   : aarch64-apple-darwin (macos-aarch64)
    Rustc    : 1.95.0 (59807616 2026-04-14)
    Backtrace: None

Ya
    Version: 26.5.6 (Homebrew 2026-05-05)

Emulator
    TERM                : Some("xterm-kitty")
    TERM_PROGRAM        : None
    TERM_PROGRAM_VERSION: None
    Brand.from_env      : Some(Kitty)
    Emulator.detect     : Emulator { kind: Left(Kitty), version: "kitty(0.46.2)", light: false, csi_16t: (15, 31), force_16t: false }

Adapter
    Adapter.matches    : Kgp
    Dimension.available: Dimension { rows: 50, columns: 193, width: 2895, height: 1550 }

Desktop
    XDG_SESSION_TYPE           : None
    WAYLAND_DISPLAY            : None
    DISPLAY                    : None
    SWAYSOCK                   : None
    HYPRLAND_INSTANCE_SIGNATURE: None
    WAYFIRE_SOCKET             : None

SSH
    shared.in_ssh_connection: false

WSL
    WSL: false

Variables
    SHELL              : Some("/bin/zsh")
    EDITOR             : Some("nvim")
    VISUAL             : Some("nvim")
    YAZI_FILE_ONE      : None
    YAZI_CONFIG_HOME   : None
    YAZI_ZOXIDE_OPTS   : None
    SSH_AUTH_SOCK      : Some("/private/tmp/com.apple.launchd.MyJnc2iptR/Listeners")
    FZF_DEFAULT_OPTS   : Some("--bind=tab:up,shift-tab:down --style=default --preview=\"[[ -f {} ]] && bat --color=always --style=numbers --line-range=:500 {} || [[ -d {} ]] && eza -1 --color=always {} || [[ {} == http* ]] && curl -sL --max-time 3 {} | w3m -dump -T text/html | head -80 || echo {}\"")
    FZF_DEFAULT_COMMAND: Some("fd")

Text Opener
    default     : Some(OpenerRule { id: Id(24), run: NonEmptyString("${EDITOR:-nvim} %s"), block: true, orphan: false, desc: "$EDITOR", for: Unix, spread: true })
    block-create: Some(OpenerRule { id: Id(24), run: NonEmptyString("${EDITOR:-nvim} %s"), block: true, orphan: false, desc: "$EDITOR", for: Unix, spread: true })
    block-rename: Some(OpenerRule { id: Id(24), run: NonEmptyString("${EDITOR:-nvim} %s"), block: true, orphan: false, desc: "$EDITOR", for: Unix, spread: true })

Multiplexers
    TMUX               : false
    tmux version       : tmux 3.6b
    tmux build flags   : enable-sixel=Supported
    ZELLIJ_SESSION_NAME: None
    Zellij version     : No such file or directory (os error 2)

Dependencies
    file          : 5.41
    ueberzugpp    : No such file or directory (os error 2)
    ffmpeg/ffprobe: 8.1.1 / 8.1.1
    pdftoppm      : No such file or directory (os error 2)
    magick        : 7.1.2-24
    fzf           : 0.73.1
    fd/fdfind     : 10.4.2 / No such file or directory (os error 2)
    rg            : 15.1.0
    chafa         : No such file or directory (os error 2)
    zoxide        : 0.9.9
    7zz/7z        : 26.01 / No such file or directory (os error 2)
    resvg         : No such file or directory (os error 2)
    jq            : 1.7.1

Clipboard
    wl-copy/paste: No such file or directory (os error 2) / No such file or directory (os error 2)
    xclip        : No such file or directory (os error 2)
    xsel         : No such file or directory (os error 2)

Routine
    `file -bL --mime-type`: text/plain


See https://yazi-rs.github.io/docs/plugins/overview#debugging on how to enable logging or debug runtime errors.

Please describe the problem you're trying to solve

Currently, bulk rename fails when the rename operations contain a cycle. For example, I want to rename:

  • file1file2
  • file2file3
  • file3file1

The goal is to support cyclic renames during bulk rename.

Would you be willing to contribute this feature?

  • Yes, I'll give it a shot

Describe the solution you'd like

My idea is to modify this function so that it returns both normal rename chains and cycle rename groups:

fn prioritized_paths(old: Vec<Tuple>, new: Vec<Tuple>) -> Vec<(Tuple, Tuple)> {
let orders: HashMap<_, _> = old.iter().enumerate().map(|(i, t)| (t, i)).collect();
let mut incomes: HashMap<_, _> = old.iter().map(|t| (t, false)).collect();
let mut todos: HashMap<_, _> = old
.iter()
.zip(new)
.map(|(o, n)| {
incomes.get_mut(&n).map(|b| *b = true);
(o, n)
})
.collect();

Normal chains can be processed as they are. For cycle groups, add a temporary rename to break the cycle. For example, the rename order will be:

  • file1.tmp
  • file3file1
  • file2file3
  • .tmpfile2

This function can be used to produce the cycle groups, and it reuses the todos and orders in prioritized_paths.

fn partition_cycles(
	mut todos: HashMap<&Tuple, Tuple>,
	orders: &HashMap<&Tuple, usize>,
) -> Vec<Renames> {
	let mut starts = todos.keys().copied().collect();
	starts.sort_unstable_by(|a, b| orders[a].cmp(&orders[b]));

	let mut cycles = Vec::new();
	for start in starts {
		if !todos.contains_key(start) {
			continue;
		}

		let mut cycle = Vec::new();
		let mut cur = start;
		while let Some(next) = todos.remove(cur) {
			cycle.push((cur.clone(), next.clone()));
			let Some((&owner, _)) = todos.get_key_value(&next) else { break };
			cur = owner;
		}
		cycles.push(cycle);
	}
	cycles
}

Additional context

I saw this issue but it was closed #3718.

Checklist

  • I have searched the existing issues/discussions
  • The latest nightly build doesn't already have this feature

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions