From 4a128fc1f5eaa4d8ef56c60342b8561269e48f3c Mon Sep 17 00:00:00 2001 From: greg Date: Sat, 4 Jul 2026 21:39:28 +0200 Subject: [PATCH] feat: add show_icons and truncate_paths options to [confirm] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 1 + Cargo.lock | 1 + yazi-config/Cargo.toml | 1 + yazi-config/preset/yazi-default.toml | 3 ++ yazi-config/src/popup/confirm.rs | 3 ++ yazi-config/src/popup/options.rs | 64 ++++++++++++++++++++++++---- 6 files changed, 64 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7187fe0a..1e68357a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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]) diff --git a/Cargo.lock b/Cargo.lock index 388e27a5..d59212f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4882,6 +4882,7 @@ dependencies = [ "serde_with", "toml", "tracing", + "unicode-width", "yazi-binding", "yazi-codegen", "yazi-fs", diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index d50defa4..0bb57ff4 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -39,3 +39,4 @@ serde = { workspace = true } serde_with = { workspace = true } toml = { workspace = true } tracing = { workspace = true } +unicode-width = { workspace = true } diff --git a/yazi-config/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index feb359c4..e5db1ef6 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -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" diff --git a/yazi-config/src/popup/confirm.rs b/yazi-config/src/popup/confirm.rs index 3cd8a989..360601fb 100644 --- a/yazi-config/src/popup/confirm.rs +++ b/yazi-config/src/popup/confirm.rs @@ -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, diff --git a/yazi-config/src/popup/options.rs b/yazi-config/src/popup/options.rs index 7b2587b8..62b4a381 100644 --- a/yazi-config/src/popup/options.rs +++ b/yazi-config/src/popup/options.rs @@ -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> { + fn truncate_files(files: &[File], max: usize, width: u16) -> Option> { + 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..]) + } }