Skip to content

fs.unique splits compound file suffixes like .tar.gz #4014

@ArtiArtem8

Description

@ArtiArtem8

ya env output

Yazi
    Version: 26.5.6 (8e80798 2026-06-01)
    Debug  : false
    Triple : x86_64-unknown-linux-gnu (linux-x86_64)
    Rustc  : 1.96.0 (ac68faa2 2026-05-25)
    Backtrace: None

Ya
    Version: 26.5.6 (8e80798 2026-06-01)
    Debug  : false
    Triple : x86_64-unknown-linux-gnu (linux-x86_64)
    Rustc  : 1.96.0 (ac68faa2 2026-05-25)

Config
    Init             : /home/arti/.config/yazi/init.lua (195 chars)
    Yazi             : /home/arti/.config/yazi/yazi.toml (63 chars)
    Keymap           : /home/arti/.config/yazi/keymap.toml (1828 chars)
    Theme            : /home/arti/.config/yazi/theme.toml (No such file or directory (os error 2))
    VFS              : /home/arti/.config/yazi/vfs.toml (No such file or directory (os error 2))
    Package          : /home/arti/.config/yazi/package.toml (624 chars)
    Dark/light flavor: ArcSwapAny("") / ArcSwapAny("")

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.47.1)", light: false, csi_16t: (9, 20), force_16t: false }

Adapter
    Adapter.matches    : Kgp
    Dimension.available: Dimension { cols: 211, rows: 50, width: 1899, height: 1000 }

Desktop
    XDG_SESSION_TYPE           : Some("wayland")
    WAYLAND_DISPLAY            : Some("wayland-1")
    DISPLAY                    : Some(":0")
    NIRI_SOCKET                : Some("/run/user/1000/niri.wayland-1.1548.sock")
    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("helix")
    VISUAL             : Some("helix")
    YAZI_FILE_ONE      : None
    YAZI_CONFIG_HOME   : None
    YAZI_ZOXIDE_OPTS   : None
    SSH_AUTH_SOCK      : None
    FZF_DEFAULT_OPTS   : None
    FZF_DEFAULT_COMMAND: None

Text Opener
    default     : Some(OpenerRule { id: Id(3), run: NonEmptyString("${EDITOR:-vi} %s"), block: true, orphan: false, desc: "$EDITOR", for: Unix, spread: true })
    block-create: Some(OpenerRule { id: Id(3), run: NonEmptyString("${EDITOR:-vi} %s"), block: true, orphan: false, desc: "$EDITOR", for: Unix, spread: true })
    block-rename: Some(OpenerRule { id: Id(3), run: NonEmptyString("${EDITOR:-vi} %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.47
    ueberzugpp    : No such file or directory (os error 2)
    ffmpeg/ffprobe: 8.1.1 / 8.1.1
    pdftoppm      : 26.05.0
    magick        : 7.1.2-24
    fzf           : 0.72.0
    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        : No such file or directory (os error 2) / 26.01
    resvg         : No such file or directory (os error 2)
    jq            : 1.8.1

Clipboard
    wl-copy/paste: 2.3.0 / 2.3.0
    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

Please describe the problem you're trying to solve

fs.unique("file", url) currently inserts the counter before the last extension only.

If I copy then paste this file in the same directory

backup.tar.gz

I get

backup.tar_1.gz

it is not a bug, but in my opinion this is a bad ux.
same kind of thing happens with:

backup.tar.zst
backup.tar.xz
package.pkg.tar.zst

I think this is a bad name. .tar.gz / .tar.zst is not just a random last extension. It is a compound suffix. backup.tar_1.gz looks like a gzip file that contains backup.tar_1, not like another backup.tar.gz archive.

Expected

backup.tar.gz
backup_1.tar.gz

backup.tar.zst
backup_1.tar.zst

It can also affect tools that look at filenames or suffixes: previewers, open rules, scripts, upload filters, archive tools, etc. For example 7z won't preview archive correctly if file suffix changed by yazi.

Would you be willing to contribute this feature?

  • Yes, I'll give it a shot

Describe the solution you'd like

I don't think fs.unique() should try to guess every possible naming pattern.

But I think there are two cases here.

First for normal file operations like copy n paste, it would be good if Yazi handled common compound file suffixes a bit better.

For example

backup.tar.gz
backup.tar.zst
backup.tar.xz
package.pkg.tar.zst

When these files are copied into the same directory, I think the counter should be inserted before the full suffix:

backup.tar.gz -> backup_1.tar.gz
backup.tar.zst -> backup_1.tar.zst
package.pkg.tar.zst -> package_1.pkg.tar.zst

not

backup.tar.gz -> backup.tar_1.gz
backup.tar.zst -> backup.tar_1.zst
package.pkg.tar.zst -> package.pkg.tar_1.zst

This should probably only be done for well known compound suffixes. I don't think every project naming style should be hardcoded into Yazi.

Second for plugins, maybe fs.unique() could accept an optional suffix from the caller.

Something like:

fs.unique("file", url, { preserve_suffix = ".tar.zst" })

or:

fs.unique("file", url, { suffix = ".tar.zst" })

Then plugins that already know what kind of file they are creating can keep the suffix intact, but still use the unique file creation from Yazi.

So the current behavior can stay for normal files:

photo.jpg -> photo_1.jpg
report.pdf -> report_1.pdf

but plugins and common archive cases can avoid breaking the suffix in the middle.

Additional context

No response

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