Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ These options are available when running with `--long` (`-l`):
- **-X**, **--dereference**: dereference symlinks for file information
- **-Z**, **--context**: list each file’s security context
- **-@**, **--extended**: list each file’s extended attributes and sizes
- **-e**, **--tags**: list each file's color tags stored in extended attributes
- **--changed**: use the changed timestamp field
- **--git**: list each file’s Git status, if tracked or ignored
- **--git-repos**: list each directory’s Git status, if tracked
Expand Down
1 change: 1 addition & 0 deletions completions/fish/eza.fish
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,5 @@ complete -c eza -l no-git -d "Suppress Git status"
complete -c eza -l git-repos -d "List each git-repos status and branch name"
complete -c eza -l git-repos-no-status -d "List each git-repos branch name (much faster)"
complete -c eza -s '@' -l extended -d "List each file's extended attributes and sizes"
complete -c eza -s e -l tags -d "List each file's color tags stored in extended attributes"
complete -c eza -s Z -l context -d "List each file's security context"
1 change: 1 addition & 0 deletions completions/nush/eza.nu
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export extern "eza" [
--git-repos # List each git-repos status and branch name
--git-repos-no-status # List each git-repos branch name (much faster)
--extended(-@) # List each file's extended attributes and sizes
--tags(-e) # List each file's color tags stored in extended attributes
--context(-Z) # List each file's security context
--smart-group # Only show group if it has a different name from owner
--stdin # When piping to eza. Read file paths from stdin
Expand Down
1 change: 1 addition & 0 deletions completions/zsh/_eza
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ __eza() {
--git-repos"[List each git-repos status and branch name]" \
--git-repos-no-status"[List each git-repos branch name (much faster)]" \
{-@,--extended}"[List each file's extended attributes and sizes]" \
{-e,--tags}"[List each file's color tags stored in extended attributes]" \
{-Z,--context}"[List each file's security context]" \
{-M,--mounts}"[Show mount details (long mode only)]" \
'*:filename:_files' \
Expand Down
23 changes: 23 additions & 0 deletions docs/theme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,26 @@ extensions:
rs: {filename: {foreground: Red}, icon: {glyph: 🦀}}
# Change the icon glyph and color
nix: {icon: {glyph: ❄, style: {foreground: White}}}
tags:
# More natural tag colors for true-color terminals
grey:
foreground: Black
background: "#8E8E93"
green:
foreground: Black
background: "#5DBC5B"
purple:
foreground: Black
background: "#A838C2"
blue:
foreground: Black
background: "#3376E8"
yellow:
foreground: Black
background: "#EDC442"
red:
foreground: Black
background: "#E14444"
orange:
foreground: Black
background: "#E68C3F"
3 changes: 3 additions & 0 deletions man/eza.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ Alternatively, `<FORMAT>` can be a two line string, the first line will be used
`-@`, `--extended`
: List each file’s extended attributes and sizes.

`-e`, `--tags`
: list each file's color tags stored in extended attributes

`-Z`, `--context`
: List each file's security context.

Expand Down
14 changes: 14 additions & 0 deletions man/eza_colors-explanation.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ file_type:
build
source

tags:
none
grey
green
purple
blue
yellow
red
orange

punctuation:

date:
Expand Down Expand Up @@ -200,6 +210,10 @@ security_context:
selinux:
role:
is_hidden: true

tags:
none:
underline: true
```

Icons can now be customized as well in the `filenames` and `extensions` fields
Expand Down
24 changes: 24 additions & 0 deletions man/eza_colors.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,30 @@ LIST OF CODES
`ff`
: BSD file flags

`Tn`
: Color of the default tag

`Tg`
: Color of the `grey` tag

`Te`
: Color of the `green` tag

`Tp`
: Color of the `purple` tag

`Tb`
: Color of the `blue` tag

`Ty`
: Color of the `yellow` tag

`Tr`
: Color of the `red` tag

`To`
: Color of the `orange` tag

Values in `EXA_COLORS` override those given in `LS_COLORS`, so you don’t need to re-write an existing `LS_COLORS` variable with proprietary extensions.


Expand Down
77 changes: 76 additions & 1 deletion src/fs/feature/xattr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#![allow(trivial_casts)] // for ARM

use crate::fs::fields::{Tag, TagColor};
use std::fmt::{Display, Formatter};
use std::io;
use std::path::Path;
Expand Down Expand Up @@ -542,7 +543,7 @@ impl Display for Attribute {
match &self.value {
None => f.write_str("<empty>"),
Some(value) => {
if let Some(val) = custom_value_display(value) {
if let Some(val) = custom_value_display(fix_trailing_zero(value)) {
f.write_fmt(format_args!("<{val}>"))
} else if let Ok(v) = str::from_utf8(value) {
f.write_fmt(format_args!("{:?}", v.trim_end_matches(char::from(0))))
Expand Down Expand Up @@ -712,3 +713,77 @@ fn plist_value_display(value: &[u8]) -> Option<String> {
.map(|s| format!("<plist version=\"1.0\">{}</plist>", s.replace('\n', "")))
})
}

/// Never happen on macOS, but happens on Linux with files and folders from macOS.
#[cfg(target_os = "macos")]
fn fix_trailing_zero(value: &[u8]) -> &[u8] {
value
}

#[cfg(not(target_os = "macos"))]
fn fix_trailing_zero(value: &[u8]) -> &[u8] {
value.strip_suffix(&[0]).unwrap_or(value)
}

pub fn display_tags(attribute: &Attribute) -> Option<Vec<Tag>> {
check_tags_name(&attribute.name)
.then(|| {
attribute
.value
.as_ref()
.filter(|value| value.starts_with(b"bplist"))
.map(|value| plist_tags_display(value))
})
.flatten()
}

const TAGS_ATTRIBUTE_NAME: &str = "com.apple.metadata:_kMDItemUserTags";

/// On macOS, the extended attribute name for tags is consistent with Finder.
#[cfg(target_os = "macos")]
fn check_tags_name(name: &str) -> bool {
name == TAGS_ATTRIBUTE_NAME
}

/// On other systems, the extended attribute name for macOS tags may vary
/// depending on the copy method (e.g. Samba, rsync).
/// Examples:
/// - "user.DosStream.com.apple.metadata:_kMDItemUserTags:$DATA"
/// - "user.com.apple.metadata:_kMDItemUserTags"
#[cfg(not(target_os = "macos"))]
fn check_tags_name(name: &str) -> bool {
name.contains(TAGS_ATTRIBUTE_NAME)
}

/// A tag is represented as a string containing:
/// - A tag name (predefined or user-defined).
/// - Optionally, a color code after a `\n`.
///
/// The numeric color code corresponds to the [`TagColor`] enum.
/// See its [`FromStr`] implementation for supported mappings.
///
/// See <https://eclecticlight.co/2024/12/24/solving-finder-tag-problems/>
fn plist_tags_display(value: &[u8]) -> Vec<Tag> {
let reader = io::Cursor::new(fix_trailing_zero(value));
plist::Value::from_reader(reader)
.ok()
.and_then(plist::Value::into_array)
.map(|arr| {
arr.into_iter()
.filter_map(|x| match x {
plist::Value::String(str) => {
let lines: Vec<&str> = str.lines().collect();
let name = lines[0].to_string();
let color = if lines.len() >= 2 {
lines[1].parse::<TagColor>().ok()
} else {
None
};
Some(Tag { name, color })
}
_ => None,
})
.collect()
})
.unwrap_or_default()
}
39 changes: 39 additions & 0 deletions src/fs/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#![allow(non_camel_case_types)]
#![allow(clippy::struct_excessive_bools)]

use std::str::FromStr;

/// The type of a file’s group ID.
pub type gid_t = u32;

Expand Down Expand Up @@ -298,3 +300,40 @@ impl Default for SubdirGitRepo {
/// The user file flags on the file. This will only ever be a number;
/// looking up the flags is done in the `display` module.
pub struct Flags(pub flag_t);

/// Tag colors in macOS.
/// <https://eclecticlight.co/2024/12/24/solving-finder-tag-problems>
#[derive(Clone)]
pub enum TagColor {
None,
Grey,
Green,
Purple,
Blue,
Yellow,
Red,
Orange,
}

impl FromStr for TagColor {
type Err = TagColor;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.parse::<u8>() {
Ok(0) => Ok(TagColor::None),
Ok(1) => Ok(TagColor::Grey),
Ok(2) => Ok(TagColor::Green),
Ok(3) => Ok(TagColor::Purple),
Ok(4) => Ok(TagColor::Blue),
Ok(5) => Ok(TagColor::Yellow),
Ok(6) => Ok(TagColor::Red),
Ok(7) => Ok(TagColor::Orange),
_ => Err(TagColor::None),
}
}
}

#[derive(Clone)]
pub struct Tag {
pub name: String,
pub color: Option<TagColor>,
}
32 changes: 31 additions & 1 deletion src/options/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
// SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors
// SPDX-FileCopyrightText: 2014 Benjamin Sago
// SPDX-License-Identifier: MIT
use crate::theme::ThemeFileType as FileType;
use crate::theme::{
FileKinds, FileNameStyle, Git, GitRepo, IconStyle, Links, Permissions, SELinuxContext,
SecurityContext, Size, UiStyles, Users,
};
use crate::theme::{Tags, ThemeFileType as FileType};
use nu_ansi_term::{Color, Style};
use serde::{Deserialize, Deserializer, Serialize};
use serde_norway;
Expand Down Expand Up @@ -538,6 +538,34 @@ impl FromOverride<FileTypeOverride> for FileType {
}
}

#[rustfmt::skip]
#[derive(Clone, Eq, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct TagsOverride {
pub none: Option<StyleOverride>, // Tn
pub grey: Option<StyleOverride>, // Tg
pub green: Option<StyleOverride>, // Te
pub purple: Option<StyleOverride>, // Tp
pub blue: Option<StyleOverride>, // Tb
pub yellow: Option<StyleOverride>, // Ty
pub red: Option<StyleOverride>, // Tr
pub orange: Option<StyleOverride>, // To
}

impl FromOverride<TagsOverride> for Tags {
fn from(value: TagsOverride, default: Self) -> Self {
Tags {
none: FromOverride::from(value.none, default.none),
grey: FromOverride::from(value.grey, default.grey),
green: FromOverride::from(value.green, default.green),
purple: FromOverride::from(value.purple, default.purple),
blue: FromOverride::from(value.blue, default.blue),
yellow: FromOverride::from(value.yellow, default.yellow),
red: FromOverride::from(value.red, default.red),
orange: FromOverride::from(value.orange, default.orange),
}
}
}

#[rustfmt::skip]
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct UiStylesOverride {
Expand All @@ -552,6 +580,7 @@ pub struct UiStylesOverride {
pub git_repo: Option<GitRepoOverride>,
pub security_context: Option<SecurityContextOverride>,
pub file_type: Option<FileTypeOverride>,
pub tags: Option<TagsOverride>,

pub punctuation: Option<StyleOverride>, // xx
pub date: Option<StyleOverride>, // da
Expand Down Expand Up @@ -584,6 +613,7 @@ impl FromOverride<UiStylesOverride> for UiStyles {
git_repo: FromOverride::from(value.git_repo, default.git_repo),
security_context: FromOverride::from(value.security_context, default.security_context),
file_type: FromOverride::from(value.file_type, default.file_type),
tags: FromOverride::from(value.tags, default.tags),

punctuation: FromOverride::from(value.punctuation, default.punctuation),
date: FromOverride::from(value.date, default.date),
Expand Down
3 changes: 2 additions & 1 deletion src/options/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub static NO_GIT: Arg = Arg { short: None, long: "no-git",
pub static GIT_REPOS: Arg = Arg { short: None, long: "git-repos", takes_value: TakesValue::Forbidden };
pub static GIT_REPOS_NO_STAT: Arg = Arg { short: None, long: "git-repos-no-status", takes_value: TakesValue::Forbidden };
pub static EXTENDED: Arg = Arg { short: Some(b'@'), long: "extended", takes_value: TakesValue::Forbidden };
pub static TAGS: Arg = Arg { short: Some(b'e'), long: "tags", takes_value: TakesValue::Forbidden };
pub static OCTAL: Arg = Arg { short: Some(b'o'), long: "octal-permissions", takes_value: TakesValue::Forbidden };
pub static SECURITY_CONTEXT: Arg = Arg { short: Some(b'Z'), long: "context", takes_value: TakesValue::Forbidden };
pub static STDIN: Arg = Arg { short: None, long: "stdin", takes_value: TakesValue::Forbidden };
Expand All @@ -113,5 +114,5 @@ pub static ALL_ARGS: Args = Args(&[
&NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &SMART_GROUP, &NO_SYMLINKS, &SHOW_SYMLINKS,

&GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT,
&EXTENDED, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS
&EXTENDED, &TAGS, &OCTAL, &SECURITY_CONTEXT, &STDIN, &FILE_FLAGS
]);
3 changes: 3 additions & 0 deletions src/options/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ static GIT_VIEW_HELP: &str = " \
";
static EXTENDED_HELP: &str = " \
-@, --extended list each file's extended attributes and sizes";
static TAGS_HELP: &str = " \
-e, --tags list each file's color tags stored in extended attributes";
static SECATTR_HELP: &str = " \
-Z, --context list each file's security context";

Expand Down Expand Up @@ -144,6 +146,7 @@ impl fmt::Display for HelpString {

if xattr::ENABLED {
write!(f, "\n{EXTENDED_HELP}")?;
write!(f, "\n{TAGS_HELP}")?;
write!(f, "\n{SECATTR_HELP}")?;
}

Expand Down
2 changes: 2 additions & 0 deletions src/options/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ impl details::Options {
table: None,
header: false,
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
tags: xattr::ENABLED && matches.has(&flags::TAGS)?,
secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
mounts: matches.has(&flags::MOUNTS)?,
color_scale: ColorScaleOptions::deduce(matches, vars)?,
Expand All @@ -186,6 +187,7 @@ impl details::Options {
table: Some(TableOptions::deduce(matches, vars)?),
header: matches.has(&flags::HEADER)?,
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
tags: xattr::ENABLED && matches.has(&flags::TAGS)?,
secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
mounts: matches.has(&flags::MOUNTS)?,
color_scale: ColorScaleOptions::deduce(matches, vars)?,
Expand Down
Loading