mirror of
https://github.com/eza-community/eza.git
synced 2026-10-11 22:30:00 +00:00
feat: add --short-nix to abbreviate Nix store hashes
A path component starting with a Nix store hash — exactly 32
characters of Nix's base32 alphabet followed by a dash — is
displayed with the hash shortened to its first 8 characters plus an
ellipsis, painted dim so the actual name stands out:
vlkia5wk0svsikwv50554mh06iayg2m2-source.drv
-> vlkia5wk…-source.drv
This applies everywhere names are painted: listings in every view,
symbolic link targets, parent paths of targets, and --absolute
paths. The matcher requires the exact hash length and alphabet
(which has no e, o, t, or u), so ordinary file names are never
false-positively shortened.
This commit is contained in:
committed by
Christina Sørensen
parent
0864331165
commit
1b9151491c
@@ -37,6 +37,7 @@ complete -c eza -l icons -d "When to display icons" -x -a "
|
||||
never\t'Never display icons'
|
||||
"
|
||||
complete -c eza -l no-quotes -d "Don't quote file names with spaces"
|
||||
complete -c eza -l short-nix -d "Abbreviate Nix store hashes in file names and paths"
|
||||
complete -c eza -l hyperlink -d "When to display entries as hyperlinks" -x -a "
|
||||
always\t'Always display entries as hyperlinks'
|
||||
auto\t'Display hyperlinks if standard output is a terminal'
|
||||
|
||||
@@ -17,6 +17,7 @@ export extern "eza" [
|
||||
--colour-scale-mode # Use gradient or fixed colors in --colour-scale
|
||||
--icons # When to display icons
|
||||
--no-quotes # Don't quote file names with spaces
|
||||
--short-nix # Abbreviate Nix store hashes in file names and paths
|
||||
--hyperlink # When to display entries as hyperlinks
|
||||
--absolute # Display entries with their absolute path
|
||||
--follow-symlinks # Drill down into symbolic links that point to directories
|
||||
|
||||
@@ -176,6 +176,7 @@ Register-ArgumentCompleter -Native -CommandName 'eza' -ScriptBlock {
|
||||
# [CompletionResult]::new('--colour-scale-mode' ,'colorscalemode' , [CompletionResultType]::ParameterName, 'use gradient or fixed colors in --color-scale (fixed, gradient)')
|
||||
[CompletionResult]::new('--icons' ,'icons' , [CompletionResultType]::ParameterName, 'when to display icons (always, auto, never)')
|
||||
[CompletionResult]::new('--no-quotes' ,'noquotes' , [CompletionResultType]::ParameterName, 'don''t quote file names with spaces')
|
||||
[CompletionResult]::new('--short-nix' ,'shortnix' , [CompletionResultType]::ParameterName, 'abbreviate Nix store hashes in file names and paths')
|
||||
[CompletionResult]::new('--hyperlink' ,'hyperlink' , [CompletionResultType]::ParameterName, 'when to display entries as hyperlinks (always, auto, never)')
|
||||
[CompletionResult]::new('--absolute' ,'absolute' , [CompletionResultType]::ParameterName, 'display entries with their absolute path (on, follow, off)')
|
||||
[CompletionResult]::new('--follow-symlinks' ,'followsymlinks' , [CompletionResultType]::ParameterName, 'drill down into symbolic links that point to directories')
|
||||
|
||||
@@ -25,6 +25,7 @@ __eza() {
|
||||
--colo{,u}r-scale-mode"[Use gradient or fixed colors in --color-scale]:(mode):(fixed gradient)" \
|
||||
--icons="[When to display icons]:(when):(always auto automatic never)" \
|
||||
--no-quotes"[Don't quote filenames with spaces]" \
|
||||
--short-nix"[Abbreviate Nix store hashes in file names and paths]" \
|
||||
--hyperlink="[When to display entries as hyperlinks]:(when):(always auto automatic never)" \
|
||||
--absolute"[Display entries with their absolute path]:(mode):(on follow off)" \
|
||||
--follow-symlinks"[Drill down into symbolic links that point to directories]" \
|
||||
|
||||
@@ -128,6 +128,11 @@ When used without a value, defaults to ‘`automatic`’.
|
||||
`--no-quotes`
|
||||
: Don't quote file names with spaces.
|
||||
|
||||
`--short-nix`
|
||||
: Abbreviate Nix store hashes in file names and paths.
|
||||
|
||||
: A path component beginning with a Nix store hash — exactly 32 characters of Nix’s base32 alphabet followed by a dash, like `vlkia5wk0svsikwv50554mh06iayg2m2-source.drv` — is displayed with the hash shortened to its first 8 characters and an ellipsis, painted dim so the name stands out: `vlkia5wk…-source.drv`. This applies to listed names, symbolic link targets, and absolute paths.
|
||||
|
||||
`--hyperlink=WHEN`
|
||||
: Display entries as hyperlinks
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ impl Options {
|
||||
let embed_hyperlinks = EmbedHyperlinks::deduce(matches);
|
||||
|
||||
let absolute = *matches.get_one("absolute").unwrap();
|
||||
let short_nix = matches.get_flag("short-nix");
|
||||
|
||||
Ok(Self {
|
||||
classify,
|
||||
@@ -32,6 +33,7 @@ impl Options {
|
||||
quote_style,
|
||||
embed_hyperlinks,
|
||||
absolute,
|
||||
short_nix,
|
||||
is_a_tty,
|
||||
})
|
||||
}
|
||||
@@ -272,8 +274,18 @@ mod tests {
|
||||
quote_style: QuoteStyle::QuoteSpaces,
|
||||
embed_hyperlinks: EmbedHyperlinks::Never,
|
||||
absolute: Absolute::Off,
|
||||
short_nix: false,
|
||||
is_a_tty: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deduce_options_short_nix() {
|
||||
assert!(
|
||||
Options::deduce(&mock_cli(vec!["--short-nix"]), &MockVars::default(), true)
|
||||
.unwrap()
|
||||
.short_nix
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ pub fn get_command() -> clap::Command {
|
||||
.value_parser(value_parser!(ShowWhen))
|
||||
.default_missing_value("auto"))
|
||||
.arg(arg!(--"no-quotes" "don't quote file names with spaces"))
|
||||
.arg(arg!(--"short-nix" "abbreviate Nix store hashes in file names and paths"))
|
||||
|
||||
.next_help_heading("FILTERING OPTIONS")
|
||||
.arg(arg!(-a --all... "show hidden files. Use this twice to also show the '.' and '..' directories"))
|
||||
|
||||
+192
-9
@@ -36,10 +36,73 @@ pub struct Options {
|
||||
/// Whether to display files with their absolute path.
|
||||
pub absolute: Absolute,
|
||||
|
||||
/// Whether to abbreviate Nix store hashes in file names and paths.
|
||||
pub short_nix: bool,
|
||||
|
||||
/// Whether we are in a console or redirecting the output
|
||||
pub is_a_tty: bool,
|
||||
}
|
||||
|
||||
/// How many characters of a Nix store hash to keep when `--short-nix`
|
||||
/// abbreviates it. Eight is enough to stay recognisable and unambiguous at
|
||||
/// a glance, like a short git commit id.
|
||||
const SHORT_NIX_HASH_LENGTH: usize = 8;
|
||||
|
||||
/// Whether this byte belongs to Nix’s base32 alphabet, which deliberately
|
||||
/// omits `e`, `o`, `t`, and `u` to avoid accidentally spelling words.
|
||||
const fn is_nix_base32(b: u8) -> bool {
|
||||
matches!(b, b'0'..=b'9' | b'a'..=b'd' | b'f'..=b'n' | b'p'..=b's' | b'v'..=b'z')
|
||||
}
|
||||
|
||||
/// If `component` starts with a Nix store hash — exactly 32 characters of
|
||||
/// Nix base32 followed by a dash — return the hash and the rest.
|
||||
fn split_nix_hash(component: &str) -> Option<(&str, &str)> {
|
||||
let (hash, rest) = component.split_at_checked(32)?;
|
||||
if rest.starts_with('-') && hash.bytes().all(is_nix_base32) {
|
||||
Some((hash, rest))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// One piece of a display name: either an abbreviated Nix store hash, to be
|
||||
/// painted dim, or ordinary text painted in the file’s usual style.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum NameSegment {
|
||||
Hash(String),
|
||||
Text(String),
|
||||
}
|
||||
|
||||
/// Split a display name (which may be a whole path) into segments, replacing
|
||||
/// every path component’s leading Nix store hash with its abbreviation.
|
||||
fn shorten_nix_segments(name: &str) -> Vec<NameSegment> {
|
||||
let mut segments = Vec::new();
|
||||
let mut text = String::new();
|
||||
|
||||
for (i, component) in name.split('/').enumerate() {
|
||||
if i > 0 {
|
||||
text.push('/');
|
||||
}
|
||||
if let Some((hash, rest)) = split_nix_hash(component) {
|
||||
if !text.is_empty() {
|
||||
segments.push(NameSegment::Text(std::mem::take(&mut text)));
|
||||
}
|
||||
segments.push(NameSegment::Hash(format!(
|
||||
"{}…",
|
||||
&hash[..SHORT_NIX_HASH_LENGTH]
|
||||
)));
|
||||
text.push_str(rest);
|
||||
} else {
|
||||
text.push_str(component);
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
segments.push(NameSegment::Text(text));
|
||||
}
|
||||
segments
|
||||
}
|
||||
|
||||
impl Options {
|
||||
/// Create a new `FileName` that prints the given file’s name, painting it
|
||||
/// with the remaining arguments.
|
||||
@@ -284,6 +347,7 @@ impl<C: Colours> FileName<'_, '_, C> {
|
||||
embed_hyperlinks: EmbedHyperlinks::Never,
|
||||
is_a_tty: self.options.is_a_tty,
|
||||
absolute: Absolute::Off,
|
||||
short_nix: self.options.short_nix,
|
||||
};
|
||||
|
||||
let target_name = FileName {
|
||||
@@ -354,8 +418,19 @@ impl<C: Colours> FileName<'_, '_, C> {
|
||||
.paint(std::path::MAIN_SEPARATOR.to_string()),
|
||||
);
|
||||
} else if coconut >= 1 {
|
||||
let mut parent_str = parent.to_string_lossy().to_string();
|
||||
if self.options.short_nix {
|
||||
// Parent paths are painted in one dim style already, so the
|
||||
// hashes just get abbreviated in place.
|
||||
parent_str = shorten_nix_segments(&parent_str)
|
||||
.into_iter()
|
||||
.map(|segment| match segment {
|
||||
NameSegment::Hash(s) | NameSegment::Text(s) => s,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
escape(
|
||||
parent.to_string_lossy().to_string(),
|
||||
parent_str,
|
||||
bits,
|
||||
self.colours.symlink_path(),
|
||||
self.colours.control_char(),
|
||||
@@ -435,13 +510,33 @@ impl<C: Colours> FileName<'_, '_, C> {
|
||||
display_hyperlink = true;
|
||||
}
|
||||
|
||||
escape(
|
||||
self.display_name(),
|
||||
&mut bits,
|
||||
file_style,
|
||||
self.colours.control_char(),
|
||||
self.options.quote_style,
|
||||
);
|
||||
let display_name = self.display_name();
|
||||
if self.options.short_nix {
|
||||
// Abbreviated store hashes get painted dim, so the part of the
|
||||
// name that a human actually reads is the part that stands out.
|
||||
for segment in shorten_nix_segments(&display_name) {
|
||||
match segment {
|
||||
NameSegment::Hash(hash) => {
|
||||
bits.push(self.colours.nix_hash().paint(hash));
|
||||
}
|
||||
NameSegment::Text(text) => escape(
|
||||
text,
|
||||
&mut bits,
|
||||
file_style,
|
||||
self.colours.control_char(),
|
||||
self.options.quote_style,
|
||||
),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
escape(
|
||||
display_name,
|
||||
&mut bits,
|
||||
file_style,
|
||||
self.colours.control_char(),
|
||||
self.options.quote_style,
|
||||
);
|
||||
}
|
||||
|
||||
if display_hyperlink {
|
||||
bits.push(ANSIString::from(escape::HYPERLINK_CLOSING));
|
||||
@@ -504,7 +599,17 @@ impl<C: Colours> FileName<'_, '_, C> {
|
||||
/// For grid's use, to cover the case of hyperlink escape sequences
|
||||
#[must_use]
|
||||
pub fn bare_utf8_width(&self) -> usize {
|
||||
UnicodeWidthStr::width(self.file.name.as_str())
|
||||
if self.options.short_nix {
|
||||
let shortened: String = shorten_nix_segments(&self.file.name)
|
||||
.into_iter()
|
||||
.map(|segment| match segment {
|
||||
NameSegment::Hash(s) | NameSegment::Text(s) => s,
|
||||
})
|
||||
.collect();
|
||||
UnicodeWidthStr::width(shortened.as_str())
|
||||
} else {
|
||||
UnicodeWidthStr::width(self.file.name.as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,6 +633,9 @@ pub trait Colours: FiletypeColours {
|
||||
/// The style to paint a non-displayable control character in a filename.
|
||||
fn control_char(&self) -> Style;
|
||||
|
||||
/// The style to paint an abbreviated Nix store hash with `--short-nix`.
|
||||
fn nix_hash(&self) -> Style;
|
||||
|
||||
/// The style to paint a non-displayable control character in a filename,
|
||||
/// when the filename is being displayed as a broken link target.
|
||||
fn broken_control_char(&self) -> Style;
|
||||
@@ -542,3 +650,78 @@ pub trait Colours: FiletypeColours {
|
||||
|
||||
fn style_override(&self, file: &File<'_>) -> Option<FileNameStyle>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
const HASH: &str = "vlkia5wk0svsikwv50554mh06iayg2m2";
|
||||
|
||||
#[test]
|
||||
fn splits_a_store_file_name() {
|
||||
let name = format!("{HASH}-source.drv");
|
||||
assert_eq!(split_nix_hash(&name), Some((HASH, "-source.drv")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_wrong_length_and_alphabet() {
|
||||
// Too short, no dash, and a hash containing characters outside
|
||||
// Nix’s base32 set (it has no e, o, t, or u).
|
||||
assert_eq!(split_nix_hash("abc-source.drv"), None);
|
||||
assert_eq!(split_nix_hash(HASH), None);
|
||||
assert_eq!(
|
||||
split_nix_hash("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-source.drv"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_names_pass_through_untouched() {
|
||||
assert_eq!(
|
||||
shorten_nix_segments("Cargo.toml"),
|
||||
vec![NameSegment::Text("Cargo.toml".into())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortens_a_bare_store_name() {
|
||||
assert_eq!(
|
||||
shorten_nix_segments(&format!("{HASH}-source.drv")),
|
||||
vec![
|
||||
NameSegment::Hash("vlkia5wk…".into()),
|
||||
NameSegment::Text("-source.drv".into()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortens_every_component_of_a_path() {
|
||||
let path = format!("/nix/store/{HASH}-foo/bin/{HASH}-bar");
|
||||
assert_eq!(
|
||||
shorten_nix_segments(&path),
|
||||
vec![
|
||||
NameSegment::Text("/nix/store/".into()),
|
||||
NameSegment::Hash("vlkia5wk…".into()),
|
||||
NameSegment::Text("-foo/bin/".into()),
|
||||
NameSegment::Hash("vlkia5wk…".into()),
|
||||
NameSegment::Text("-bar".into()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_mid_component_is_not_shortened() {
|
||||
let name = format!("prefix-{HASH}-source.drv");
|
||||
assert_eq!(
|
||||
shorten_nix_segments(&name),
|
||||
vec![NameSegment::Text(name.clone())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multibyte_names_are_split_safely() {
|
||||
// A name whose 32-char boundary would fall inside a multi-byte
|
||||
// character must not panic.
|
||||
assert_eq!(split_nix_hash("ααααααααααααααααα-x"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +469,7 @@ impl FileNameColours for Theme {
|
||||
fn broken_filename(&self) -> Style { apply_overlay(self.ui.broken_symlink(), self.ui.broken_path_overlay()) }
|
||||
fn control_char(&self) -> Style { self.ui.control_char() }
|
||||
fn broken_control_char(&self) -> Style { apply_overlay(self.ui.control_char(), self.ui.broken_path_overlay()) }
|
||||
fn nix_hash(&self) -> Style { self.ui.punctuation() }
|
||||
fn executable_file(&self) -> Style { self.ui.filekinds.unwrap_or_default().executable() }
|
||||
fn mount_point(&self) -> Style { self.ui.filekinds.unwrap_or_default().mount_point() }
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ DISPLAY OPTIONS:
|
||||
--icons [<WHEN>] when to display icons [possible values: always, auto, never]
|
||||
--hyperlink [<WHEN>] when to display entries as hyperlinks [possible values: always, auto, never]
|
||||
--no-quotes don't quote file names with spaces
|
||||
--short-nix abbreviate Nix store hashes in file names and paths
|
||||
|
||||
FILTERING OPTIONS:
|
||||
-a, --all... show hidden files. Use this twice to also show the '.' and '..' directories
|
||||
|
||||
Reference in New Issue
Block a user