diff --git a/src/config.rs b/src/config.rs index 567c173a..eadb7565 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,6 +14,10 @@ use std::{fmt, path::PathBuf, str::FromStr}; /// ```toml /// format = "binary" /// +/// # Show directories as `dir/` instead of `/dir` in interactive mode. +/// # If unset, behavior defaults to false. +/// # directory_suffix = false +/// /// # Controls whether Git-ignored entry detection is enabled in interactive mode. /// # Supported values: true, false. /// # If unset, behavior defaults to true. @@ -38,6 +42,10 @@ pub struct Config { /// Byte count format to use when `--format` and `DUA_FORMAT` are not set. pub format: Option, + /// Whether directories are shown as `dir/` instead of `/dir` in interactive mode. + /// If unset, defaults to `false`. + pub directory_suffix: bool, + /// Keybinding-related settings. pub keys: KeysConfig, @@ -616,6 +624,10 @@ impl Config { "# Supported values: metric, binary, bytes, gb, gib, mb, mib.\n", "# format = \"binary\"\n", "#\n", + "# Show directories as `dir/` instead of `/dir` (NCDU style) in interactive mode.\n", + "# If unset, behavior defaults to false.\n", + "# directory_suffix = false\n", + "#\n", "# Controls whether Git-ignored entry detection is enabled in interactive mode.\n", "# Supported values: true, false.\n", "# If unset, behavior defaults to true.\n", diff --git a/src/interactive/widgets/entries.rs b/src/interactive/widgets/entries.rs index 4435a20d..d1373752 100644 --- a/src/interactive/widgets/entries.rs +++ b/src/interactive/widgets/entries.rs @@ -31,6 +31,8 @@ pub struct EntriesProps<'a> { pub current_path: PathBuf, /// Size display mode used for byte and percentage columns. pub display: DisplayOptions, + /// Whether directories are shown as `dir/` instead of `/dir`. + pub directory_suffix: bool, /// Currently selected tree entry, if one is selected. pub selected: Option, /// Entries to display in the pane, already sorted for the current view. @@ -70,6 +72,7 @@ impl Entries { let EntriesProps { current_path, display, + directory_suffix, entries, selected, marked, @@ -154,7 +157,7 @@ impl Entries { ) as usize; let name = shorten_input( - name_with_prefix(name.to_string_lossy(), *is_dir), + name_with_directory_marker(name.to_string_lossy(), *is_dir, *directory_suffix), available_width, ); let style = name_style( @@ -571,34 +574,26 @@ fn fill_background_to_right(mut s: Cow<'_, str>, entire_width: u16) -> Cow<'_, s } } -fn name_with_prefix(mut name: Cow<'_, str>, is_dir: bool) -> Cow<'_, str> { - let prefix = if is_dir { - // Note that these names never happen on non-root items, so this is a root-item special case. - // It was necessary since we can't trust the 'actual' root anymore as it might be the CWD or - // `main()` cwd' into the one path that was provided by the user. - // The idea was to keep explicit roots as specified without adjustment, which works with this - // logic unless somebody provides `name` as is, then we will prefix it which is a little confusing. - // Overall, this logic makes the folder display more consistent. - if name == "." - || name == ".." - || name.starts_with('/') - || name.starts_with("./") - || name.starts_with("../") - { - None - } else { - Some("/") - } - } else { - Some(" ") - }; - match prefix { - None => name, - Some(prefix) => { - name.to_mut().insert_str(0, prefix); - name +fn name_with_directory_marker( + mut name: Cow<'_, str>, + is_dir: bool, + directory_suffix: bool, +) -> Cow<'_, str> { + if directory_suffix { + if is_dir && !name.chars().last().is_some_and(std::path::is_separator) { + name.to_mut().push('/'); } - } + } else if !is_dir { + name.to_mut().insert(0, ' '); + } else if name != "." + && name != ".." + && !name.starts_with('/') + && !name.starts_with("./") + && !name.starts_with("../") + { + name.to_mut().insert(0, '/'); + } + name } fn name_style( @@ -776,7 +771,10 @@ mod entries_test { use std::collections::HashSet; use std::path::Path; - use super::{name_style, shorten_input, show_mtime_column, title as entry_title}; + use super::{ + name_style, name_with_directory_marker, shorten_input, show_mtime_column, + title as entry_title, + }; use crate::interactive::widgets::{Column, Language}; use crate::interactive::{MTimeSort, SortMode}; use dua::ByteFormat; @@ -811,6 +809,25 @@ mod entries_test { } } + #[test] + fn directory_marker_can_be_moved_to_a_suffix_by_config() { + let suffix = toml::from_str::("directory_suffix = true").unwrap(); + for (config, directory, file) in [ + (dua::Config::default(), "/dir", " file"), + (suffix, "dir/", "file"), + ] { + assert_eq!( + name_with_directory_marker("dir".into(), true, config.directory_suffix), + directory + ); + assert_eq!( + name_with_directory_marker("file".into(), false, config.directory_suffix), + file + ); + } + assert_eq!(name_with_directory_marker("/".into(), true, true), "/"); + } + #[test] fn title_drops_statistics_before_shortening_path() { let path = "a/b/c"; diff --git a/src/interactive/widgets/main.rs b/src/interactive/widgets/main.rs index d2efa1bc..4cf599b1 100644 --- a/src/interactive/widgets/main.rs +++ b/src/interactive/widgets/main.rs @@ -115,6 +115,7 @@ impl MainWindow { let props = EntriesProps { current_path: current_path.clone(), display: *display, + directory_suffix: config.directory_suffix, entries: &state.entries, marked, cleanup_candidates: state.cleanup_candidates.as_ref(),