feat: add show_icons and truncate_paths options to [confirm]

The trash/delete/overwrite confirmation dialogs (icons added in #4096)
had no way to disable icons, and long file paths would hard word-wrap
onto a new line right after the icon since a path has no internal
spaces to break on cleanly.

Add two options under [confirm], both independently toggleable from the
existing [input] show_icons:

- show_icons (default true): skip the icon lookup for confirm dialogs.
- truncate_paths (default false): RTL-truncate each path with a leading
  "…" to fit the popup's configured width, instead of word-wrapping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
greg 2026-07-04 21:39:28 +02:00
parent b53a1b2ea7
commit 4a128fc1f5
6 changed files with 64 additions and 9 deletions

View file

@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- H/M/L Vim-like motion for moving cursor relative to viewport ([#3970])
- Context-aware icons for inputs ([#4080])
- New `show_icons` option under `[input]` to toggle icons on input popups ([#4100])
- New `show_icons` and `truncate_paths` options under `[confirm]` to toggle icons and RTL-ellipsis-truncate long paths on trash/delete/overwrite confirmations ([#4100])
- Show file icons in trash/delete/overwrite confirmations ([#4096])
- Dynamic keymap Lua API ([#4031])
- New `ui.Input` element ([#4040])

1
Cargo.lock generated
View file

@ -4882,6 +4882,7 @@ dependencies = [
"serde_with",
"toml",
"tracing",
"unicode-width",
"yazi-binding",
"yazi-codegen",
"yazi-fs",

View file

@ -39,3 +39,4 @@ serde = { workspace = true }
serde_with = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
unicode-width = { workspace = true }

View file

@ -214,6 +214,9 @@ shell_origin = "top-center"
shell_offset = [ 0, 2, 50, 3 ]
[confirm]
truncate_paths = false
show_icons = true
# trash
trash_title = "Trash {n} selected file{s}?"
trash_origin = "center"

View file

@ -4,6 +4,9 @@ use yazi_codegen::{DeserializeOver, DeserializeOver2};
#[derive(Deserialize, DeserializeOver, DeserializeOver2)]
pub struct Confirm {
pub truncate_paths: bool,
pub show_icons: bool,
// trash
pub trash_title: String,
pub trash_origin: Origin,

View file

@ -2,6 +2,7 @@ use std::slice;
use ratatui_core::text::{Line, Span, Text};
use ratatui_widgets::paragraph::{Paragraph, Wrap};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use yazi_binding::position::Position;
use yazi_fs::file::File;
use yazi_macro::impl_data_any;
@ -9,6 +10,11 @@ use yazi_shared::strand::ToStrand;
use crate::{THEME, YAZI};
// Mirrors the horizontal `Margin::new(2, 0)` the confirm list widget applies
// around its content (yazi-fm/src/confirm/list.rs), so a path truncated here
// to the popup's configured width doesn't still overflow once rendered.
const LIST_MARGIN: u16 = 4;
// --- PickCfg
#[derive(Clone, Debug, Default)]
pub struct PickCfg {
@ -46,29 +52,32 @@ impl ConfirmCfg {
}
pub fn trash(files: &[File]) -> Self {
let position = YAZI.confirm.trash_position();
Self::new(
Self::replace_number(&YAZI.confirm.trash_title, files.len()),
YAZI.confirm.trash_position(),
position,
None,
Self::truncate_files(files, 100),
Self::truncate_files(files, 100, position.width),
)
}
pub fn delete(files: &[File]) -> Self {
let position = YAZI.confirm.delete_position();
Self::new(
Self::replace_number(&YAZI.confirm.delete_title, files.len()),
YAZI.confirm.delete_position(),
position,
None,
Self::truncate_files(files, 100),
Self::truncate_files(files, 100, position.width),
)
}
pub fn overwrite(file: &File) -> Self {
let position = YAZI.confirm.overwrite_position();
Self::new(
YAZI.confirm.overwrite_title.clone(),
YAZI.confirm.overwrite_position(),
position,
Some(Text::raw(&YAZI.confirm.overwrite_body)),
Self::truncate_files(slice::from_ref(file), 1),
Self::truncate_files(slice::from_ref(file), 1, position.width),
)
}
@ -101,7 +110,9 @@ impl ConfirmCfg {
Some(Text::from_iter(lines))
}
fn truncate_files(files: &[File], max: usize) -> Option<Text<'static>> {
fn truncate_files(files: &[File], max: usize, width: u16) -> Option<Text<'static>> {
let budget = (width as usize).saturating_sub(LIST_MARGIN as usize);
let mut lines = Vec::with_capacity(files.len().min(max + 1));
for (i, f) in files.iter().enumerate() {
if i >= max {
@ -110,12 +121,47 @@ impl ConfirmCfg {
}
lines.push(Line::default());
if let Some(icon) = THEME.icon.matches(f, false) {
let mut prefix = 0;
if YAZI.confirm.show_icons
&& let Some(icon) = THEME.icon.matches(f, false)
{
prefix = icon.text.width() + 1;
lines[i].push_span(Span::styled(icon.text, icon.style));
lines[i].push_span(" ");
}
lines[i].push_span(f.url.to_strand().into_string_lossy());
let path = f.url.to_strand().into_string_lossy();
let path = if YAZI.confirm.truncate_paths {
Self::truncate_rtl(&path, budget.saturating_sub(prefix))
} else {
path
};
lines[i].push_span(path);
}
Some(lines.into())
}
// Truncates `s` from the left with a leading `…`, keeping its tail, since
// the end of a path is usually more identifying than its start.
fn truncate_rtl(s: &str, max: usize) -> String {
if s.width() <= max {
return s.to_owned();
}
if max == 0 {
return String::new();
}
let mut adv = 0;
let mut idx = s.len();
for (i, c) in s.char_indices().rev() {
let w = c.width().unwrap_or(0);
if adv + w > max.saturating_sub(1) {
break;
}
adv += w;
idx = i;
}
format!("{}", &s[idx..])
}
}