Переглянути джерело

Merge branch 'main' into pr-1164

Signed-off-by: Christina Sørensen <christina@cafkafk.com>
Christina Sørensen 2 роки тому
батько
коміт
b51098d344
48 змінених файлів з 580 додано та 232 видалено
  1. 1 1
      .github/workflows/unit-tests.yml
  2. 2 2
      Cargo.toml
  3. 3 2
      README.md
  4. 1 1
      build.rs
  5. 2 1
      completions/fish/exa.fish
  6. 1 0
      completions/zsh/_exa
  7. 3 0
      man/exa.1.md
  8. 1 1
      rust-toolchain.toml
  9. 1 1
      src/fs/dir.rs
  10. 2 2
      src/fs/dir_action.rs
  11. 49 0
      src/fs/feature/git.rs
  12. 6 0
      src/fs/feature/mod.rs
  13. 121 51
      src/fs/feature/xattr.rs
  14. 34 1
      src/fs/fields.rs
  15. 42 8
      src/fs/file.rs
  16. 14 14
      src/fs/filter.rs
  17. 1 1
      src/info/filetype.rs
  18. 1 1
      src/main.rs
  19. 4 4
      src/options/error.rs
  20. 0 1
      src/options/filter.rs
  21. 7 4
      src/options/flags.rs
  22. 5 4
      src/options/help.rs
  23. 3 3
      src/options/mod.rs
  24. 9 9
      src/options/parser.rs
  25. 4 4
      src/options/theme.rs
  26. 1 1
      src/options/version.rs
  27. 14 8
      src/options/view.rs
  28. 2 4
      src/output/cell.rs
  29. 30 30
      src/output/details.rs
  30. 2 2
      src/output/file_name.rs
  31. 1 1
      src/output/grid.rs
  32. 4 13
      src/output/grid_details.rs
  33. 2 2
      src/output/mod.rs
  34. 2 2
      src/output/render/blocks.rs
  35. 28 3
      src/output/render/git.rs
  36. 2 2
      src/output/render/inode.rs
  37. 3 3
      src/output/render/links.rs
  38. 3 0
      src/output/render/mod.rs
  39. 7 7
      src/output/render/octal.rs
  40. 1 0
      src/output/render/permissions.rs
  41. 45 0
      src/output/render/securityctx.rs
  42. 1 1
      src/output/render/size.rs
  43. 51 9
      src/output/table.rs
  44. 2 2
      src/output/time.rs
  45. 6 6
      src/output/tree.rs
  46. 11 0
      src/theme/default_theme.rs
  47. 23 14
      src/theme/mod.rs
  48. 22 6
      src/theme/ui_styles.rs

+ 1 - 1
.github/workflows/unit-tests.yml

@@ -28,7 +28,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest]
-        rust: [1.56.1, stable, beta, nightly]
+        rust: [1.63.0, stable, beta, nightly]
 
     steps:
       - name: Checkout repository

+ 2 - 2
Cargo.toml

@@ -3,8 +3,8 @@ name = "exa"
 description = "A modern replacement for ls"
 authors = ["Benjamin Sago <ogham@bsago.me>"]
 categories = ["command-line-utilities"]
-edition = "2018"
-rust-version = "1.56.1"
+edition = "2021"
+rust-version = "1.63.0"
 exclude = ["/devtools/*", "/Justfile", "/Vagrantfile", "/screenshots.png"]
 readme = "README.md"
 homepage = "https://the.exa.website/"

+ 3 - 2
README.md

@@ -74,6 +74,7 @@ These options are available when running with `--long` (`-l`):
 - **-t**, **--time=(field)**: which timestamp field to use
 - **-u**, **--accessed**: use the accessed timestamp field
 - **-U**, **--created**: use the created timestamp field
+- **-Z**, **--context**: list each file’s security context
 - **-@**, **--extended**: list each file’s extended attributes and sizes
 - **--changed**: use the changed timestamp field
 - **--git**: list each file’s Git status, if tracked or ignored
@@ -196,8 +197,8 @@ To build without Git support, run `cargo install --no-default-features exa` is a
 <a id="development">
 <h1>Development
 
-<a href="https://blog.rust-lang.org/2021/11/01/Rust-1.56.1.html">
-    <img src="https://img.shields.io/badge/rustc-1.56.1+-lightgray.svg" alt="Rust 1.56.1+" />
+<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html">
+    <img src="https://img.shields.io/badge/rustc-1.63.0+-lightgray.svg" alt="Rust 1.63.0+" />
 </a>
 
 <a href="https://github.com/ogham/exa/blob/master/LICENCE">

+ 1 - 1
build.rs

@@ -41,7 +41,7 @@ fn main() -> io::Result<()> {
     let path = &out.join("version_string.txt");
 
     // Bland version text
-    let mut f = File::create(path).expect(&path.to_string_lossy());
+    let mut f = File::create(path).unwrap_or_else(|_| { panic!("{}", path.to_string_lossy().to_string()) });
     writeln!(f, "{}", strip_codes(&ver))?;
 
     Ok(())

+ 2 - 1
completions/fish/exa.fish

@@ -61,7 +61,7 @@ complete -c exa -s 'B' -l 'bytes'    -d "List file sizes in bytes, without any p
 complete -c exa -s 'g' -l 'group'    -d "List each file's group"
 complete -c exa -s 'h' -l 'header'   -d "Add a header row to each column"
 complete -c exa -s 'H' -l 'links'    -d "List each file's number of hard links"
-complete -c exa -s 'g' -l 'group'    -d "List each file's inode number"
+complete -c exa -s 'i' -l 'inode'    -d "List each file's inode number"
 complete -c exa -s 'S' -l 'blocks'   -d "List each file's number of filesystem blocks"
 complete -c exa -s 't' -l 'time'     -d "Which timestamp field to list" -x -a "
     modified\t'Display modified time'
@@ -89,3 +89,4 @@ complete -c exa        -l 'no-time'           -d "Suppress the time field"
 # Optional extras
 complete -c exa -l 'git' -d "List each file's Git status, if tracked"
 complete -c exa -s '@' -l 'extended' -d "List each file's extended attributes and sizes"
+complete -c exa -s 'Z' -l 'context' -d "List each file's security context"

+ 1 - 0
completions/zsh/_exa

@@ -53,6 +53,7 @@ __exa() {
         {-U,--created}"[Use the created timestamp field]" \
         --git"[List each file's Git status, if tracked]" \
         {-@,--extended}"[List each file's extended attributes and sizes]" \
+        {-Z,--context}"[List each file's security context]" \
         '*:filename:_files'
 }
 

+ 3 - 0
man/exa.1.md

@@ -183,6 +183,9 @@ These options are available when running with `--long` (`-l`):
 `-@`, `--extended`
 : List each file’s extended attributes and sizes.
 
+`-Z`, `--context`
+: List each file's security context.
+
 `--git`  [if exa was built with git support]
 : List each file’s Git status, if tracked.
 

+ 1 - 1
rust-toolchain.toml

@@ -1,2 +1,2 @@
 [toolchain]
-channel = "1.56.1"
+channel = "1.63.0"

+ 1 - 1
src/fs/dir.rs

@@ -176,7 +176,7 @@ impl<'dir, 'ig> Iterator for Files<'dir, 'ig> {
 /// Usually files in Unix use a leading dot to be hidden or visible, but two
 /// entries in particular are “extra-hidden”: `.` and `..`, which only become
 /// visible after an extra `-a` option.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum DotFilter {
 
     /// Shows files, dotfiles, and `.` and `..`.

+ 2 - 2
src/fs/dir_action.rs

@@ -19,7 +19,7 @@
 /// into them and print out their contents. The recurse mode does this by
 /// having extra output blocks at the end, while the tree mode will show
 /// directories inline, with their contents immediately underneath.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum DirAction {
 
     /// This directory should be listed along with the regular files, instead
@@ -58,7 +58,7 @@ impl DirAction {
 
 
 /// The options that determine how to recurse into a directory.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub struct RecurseOptions {
 
     /// Whether recursion should be done as a tree or as multiple individual

+ 49 - 0
src/fs/feature/git.rs

@@ -343,3 +343,52 @@ fn index_status(status: git2::Status) -> f::GitStatus {
         _                                                => f::GitStatus::NotModified,
     }
 }
+
+fn current_branch(repo: &git2::Repository) -> Option<String>{
+    let head = match repo.head() {
+        Ok(head) => Some(head),
+        Err(ref e) if e.code() == git2::ErrorCode::UnbornBranch || e.code() == git2::ErrorCode::NotFound => return None,
+        Err(e) => {
+            error!("Error looking up Git branch: {:?}", e);
+            return None
+        }
+    };
+
+    if let Some(h) = head{
+        if let Some(s) = h.shorthand(){
+            let branch_name = s.to_owned();
+            if branch_name.len() > 10 {
+               return Some(branch_name[..8].to_string()+"..");
+            }
+            return Some(branch_name);
+        }
+    }
+    None
+}
+
+impl f::SubdirGitRepo{
+    pub fn from_path(dir : &Path, status : bool) -> Self{
+
+        let path = &reorient(&dir);
+        let g = git2::Repository::open(path);
+        if let Ok(repo) = g{
+
+            let branch = current_branch(&repo);
+            if !status{
+                return Self{status : f::SubdirGitRepoStatus::GitUnknown, branch};
+            }
+            match repo.statuses(None) {
+                Ok(es) => {
+                    if es.iter().filter(|s| s.status() != git2::Status::IGNORED).any(|_| true){
+                        return Self{status : f::SubdirGitRepoStatus::GitDirty, branch};
+                    }
+                    return Self{status : f::SubdirGitRepoStatus::GitClean, branch};
+                }
+                Err(e) => {
+                    error!("Error looking up Git statuses: {:?}", e)
+                }
+            }
+        }
+        Self::default()
+    }
+}

+ 6 - 0
src/fs/feature/mod.rs

@@ -30,4 +30,10 @@ pub mod git {
             unreachable!();
         }
     }
+
+    impl f::SubdirGitRepo{
+        pub fn from_path(_dir : &Path, _status : bool) -> Self{
+            panic!("Tried to get subdir Git status, but Git support is disabled")
+        }
+    }
 }

+ 121 - 51
src/fs/feature/xattr.rs

@@ -3,6 +3,7 @@
 #![allow(trivial_casts)]  // for ARM
 
 use std::cmp::Ordering;
+use std::ffi::CString;
 use std::io;
 use std::path::Path;
 
@@ -50,58 +51,98 @@ pub enum FollowSymlinks {
 #[derive(Debug, Clone)]
 pub struct Attribute {
     pub name: String,
-    pub size: usize,
+    pub value: String,
 }
 
 
 #[cfg(any(target_os = "macos", target_os = "linux"))]
-pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attribute>> {
-    use std::ffi::CString;
+fn get_secattr(lister: &lister::Lister, c_path: &std::ffi::CString) -> io::Result<Vec<Attribute>> {
+    const SELINUX_XATTR_NAME: &str = "security.selinux";
+    const ENODATA: i32 = 61;
 
-    let c_path = match path.to_str().and_then(|s| CString::new(s).ok()) {
-        Some(cstring) => cstring,
-        None => {
-            return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?"));
-        }
+    let c_attr_name = CString::new(SELINUX_XATTR_NAME).map_err(|e| {
+        io::Error::new(io::ErrorKind::Other, e)
+    })?;
+    let size = lister.getxattr_first(c_path, &c_attr_name);
+
+    let size = match size.cmp(&0) {
+        Ordering::Less => {
+            let e = io::Error::last_os_error();
+
+            if e.kind() == io::ErrorKind::Other && e.raw_os_error() == Some(ENODATA) {
+                return Ok(Vec::new())
+            }
+
+            return Err(e)
+        },
+        Ordering::Equal => return Err(io::Error::from(io::ErrorKind::InvalidData)),
+        Ordering::Greater => size as usize,
     };
 
+    let mut buf_value = vec![0_u8; size];
+    let size = lister.getxattr_second(c_path, &c_attr_name, &mut buf_value, size);
+
+    match size.cmp(&0) {
+        Ordering::Less => return Err(io::Error::last_os_error()),
+        Ordering::Equal => return Err(io::Error::from(io::ErrorKind::InvalidData)),
+        Ordering::Greater => (),
+    }
+
+    Ok(vec![Attribute {
+        name:  String::from(SELINUX_XATTR_NAME),
+        value: lister.translate_attribute_data(&buf_value),
+    }])
+}
+
+pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attribute>> {
+    let c_path = CString::new(path.to_str().ok_or(io::Error::new(io::ErrorKind::Other, "Error: path not convertible to string"))?).map_err(|e| {
+        io::Error::new(io::ErrorKind::Other, e)
+    })?;
+
     let bufsize = lister.listxattr_first(&c_path);
-    match bufsize.cmp(&0) {
+    let bufsize = match bufsize.cmp(&0) {
         Ordering::Less     => return Err(io::Error::last_os_error()),
-        Ordering::Equal    => return Ok(Vec::new()),
-        Ordering::Greater  => {},
-    }
+        // Some filesystems, like sysfs, return nothing on listxattr, even though the security
+        // attribute is set.
+        Ordering::Equal    => return get_secattr(lister, &c_path),
+        Ordering::Greater  => bufsize as usize,
+    };
 
-    let mut buf = vec![0_u8; bufsize as usize];
-    let err = lister.listxattr_second(&c_path, &mut buf, bufsize);
+    let mut buf = vec![0_u8; bufsize];
 
-    match err.cmp(&0) {
+    match lister.listxattr_second(&c_path, &mut buf, bufsize).cmp(&0) {
         Ordering::Less     => return Err(io::Error::last_os_error()),
         Ordering::Equal    => return Ok(Vec::new()),
         Ordering::Greater  => {},
     }
 
     let mut names = Vec::new();
-    if err > 0 {
-        // End indices of the attribute names
-        // the buffer contains 0-terminated c-strings
-        let idx = buf.iter().enumerate().filter_map(|(i, v)|
-            if *v == 0 { Some(i) } else { None }
-        );
-        let mut start = 0;
-
-        for end in idx {
-            let c_end = end + 1; // end of the c-string (including 0)
-            let size = lister.getxattr(&c_path, &buf[start..c_end]);
-
-            if size > 0 {
-                names.push(Attribute {
-                    name: lister.translate_attribute_name(&buf[start..end]),
-                    size: size as usize,
-                });
+
+    for attr_name in buf.split(|c| c == &0) {
+        if attr_name.is_empty() {
+            continue;
+        }
+
+        let c_attr_name = CString::new(attr_name).map_err(|e| {
+            io::Error::new(io::ErrorKind::Other, e)
+        })?;
+        let size = lister.getxattr_first(&c_path, &c_attr_name);
+
+        if size > 0 {
+            let mut buf_value = vec![0_u8; size as usize];
+            if lister.getxattr_second(&c_path, &c_attr_name, &mut buf_value, size as usize) < 0 {
+                return Err(io::Error::last_os_error());
             }
 
-            start = c_end;
+            names.push(Attribute {
+                name:  lister.translate_attribute_data(attr_name),
+                value: lister.translate_attribute_data(&buf_value),
+            });
+        } else {
+            names.push(Attribute {
+                name:  lister.translate_attribute_data(attr_name),
+                value: String::new(),
+            });
         }
     }
 
@@ -148,8 +189,8 @@ mod lister {
             Self { c_flags }
         }
 
-        pub fn translate_attribute_name(&self, input: &[u8]) -> String {
-            unsafe { std::str::from_utf8_unchecked(input).into() }
+        pub fn translate_attribute_data(&self, input: &[u8]) -> String {
+            unsafe { std::str::from_utf8_unchecked(input).trim_end_matches('\0').into() }
         }
 
         pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
@@ -163,22 +204,22 @@ mod lister {
             }
         }
 
-        pub fn listxattr_second(&self, c_path: &CString, buf: &mut Vec<u8>, bufsize: ssize_t) -> ssize_t {
+        pub fn listxattr_second(&self, c_path: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
             unsafe {
                 listxattr(
                     c_path.as_ptr(),
-                    buf.as_mut_ptr() as *mut c_char,
-                    bufsize as size_t,
+                    buf.as_mut_ptr().cast(),
+                    bufsize,
                     self.c_flags,
                 )
             }
         }
 
-        pub fn getxattr(&self, c_path: &CString, buf: &[u8]) -> ssize_t {
+        pub fn getxattr_first(&self, c_path: &CString, c_name: &CString) -> ssize_t {
             unsafe {
                 getxattr(
                     c_path.as_ptr(),
-                    buf.as_ptr() as *const c_char,
+                    c_name.as_ptr().cast(),
                     ptr::null_mut(),
                     0,
                     0,
@@ -186,6 +227,19 @@ mod lister {
                 )
             }
         }
+
+        pub fn getxattr_second(&self, c_path: &CString, c_name: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
+            unsafe {
+                getxattr(
+                    c_path.as_ptr(),
+                    c_name.as_ptr().cast(),
+                    buf.as_mut_ptr().cast::<libc::c_void>(),
+                    bufsize,
+                    0,
+                    self.c_flags,
+                )
+            }
+        }
     }
 }
 
@@ -234,8 +288,8 @@ mod lister {
             Lister { follow_symlinks }
         }
 
-        pub fn translate_attribute_name(&self, input: &[u8]) -> String {
-            String::from_utf8_lossy(input).into_owned()
+        pub fn translate_attribute_data(&self, input: &[u8]) -> String {
+            String::from_utf8_lossy(input).trim_end_matches('\0').into()
         }
 
         pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
@@ -246,14 +300,14 @@ mod lister {
 
             unsafe {
                 listxattr(
-                    c_path.as_ptr().cast(),
+                    c_path.as_ptr(),
                     ptr::null_mut(),
                     0,
                 )
             }
         }
 
-        pub fn listxattr_second(&self, c_path: &CString, buf: &mut Vec<u8>, bufsize: ssize_t) -> ssize_t {
+        pub fn listxattr_second(&self, c_path: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
             let listxattr = match self.follow_symlinks {
                 FollowSymlinks::Yes  => listxattr,
                 FollowSymlinks::No   => llistxattr,
@@ -261,27 +315,43 @@ mod lister {
 
             unsafe {
                 listxattr(
-                    c_path.as_ptr().cast(),
+                    c_path.as_ptr(),
                     buf.as_mut_ptr().cast(),
-                    bufsize as size_t,
+                    bufsize,
                 )
             }
         }
 
-        pub fn getxattr(&self, c_path: &CString, buf: &[u8]) -> ssize_t {
+        pub fn getxattr_first(&self, c_path: &CString, c_name: &CString) -> ssize_t {
             let getxattr = match self.follow_symlinks {
-                FollowSymlinks::Yes  => getxattr,
-                FollowSymlinks::No   => lgetxattr,
+                FollowSymlinks::Yes => getxattr,
+                FollowSymlinks::No  => lgetxattr,
             };
 
             unsafe {
                 getxattr(
-                    c_path.as_ptr().cast(),
-                    buf.as_ptr().cast(),
+                    c_path.as_ptr(),
+                    c_name.as_ptr().cast(),
                     ptr::null_mut(),
                     0,
                 )
             }
         }
+
+        pub fn getxattr_second(&self, c_path: &CString, c_name: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
+            let getxattr = match self.follow_symlinks {
+                FollowSymlinks::Yes => getxattr,
+                FollowSymlinks::No  => lgetxattr,
+            };
+
+            unsafe {
+                getxattr(
+                    c_path.as_ptr(),
+                    c_name.as_ptr().cast(),
+                    buf.as_mut_ptr().cast::<libc::c_void>(),
+                    bufsize,
+                )
+            }
+        }
     }
 }

+ 34 - 1
src/fs/fields.rs

@@ -210,7 +210,7 @@ pub struct Time {
 /// A file’s status in a Git repository. Whether a file is in a repository or
 /// not is handled by the Git module, rather than having a “null” variant in
 /// this enum.
-#[derive(PartialEq, Copy, Clone)]
+#[derive(PartialEq, Eq, Copy, Clone)]
 pub enum GitStatus {
 
     /// This file hasn’t changed since the last commit.
@@ -259,3 +259,36 @@ impl Default for Git {
         }
     }
 }
+
+pub enum SecurityContextType<'a> {
+    SELinux(&'a str),
+    None
+}
+
+pub struct SecurityContext<'a> {
+    pub context: SecurityContextType<'a>,
+}
+
+#[allow(dead_code)]
+#[derive(PartialEq, Copy, Clone)]
+pub enum SubdirGitRepoStatus{
+    NoRepo,
+    GitClean,
+    GitDirty,
+    GitUnknown
+}
+
+#[derive(Clone)]
+pub struct SubdirGitRepo{
+    pub status : SubdirGitRepoStatus,
+    pub branch : Option<String>
+}
+
+impl Default for SubdirGitRepo{
+    fn default() -> Self {
+        Self{
+            status : SubdirGitRepoStatus::NoRepo,
+            branch : None
+        }
+    }
+}

+ 42 - 8
src/fs/file.rs

@@ -11,6 +11,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
 use log::*;
 
 use crate::fs::dir::Dir;
+use crate::fs::feature::xattr;
+use crate::fs::feature::xattr::{FileAttributes, Attribute};
 use crate::fs::fields as f;
 
 
@@ -66,6 +68,9 @@ pub struct File<'dir> {
     /// directory’s children, and are in fact added specifically by exa; this
     /// means that they should be skipped when recursing.
     pub is_all_all: bool,
+
+    /// The extended attributes of this file.
+    pub extended_attributes: Vec<Attribute>,
 }
 
 impl<'dir> File<'dir> {
@@ -80,8 +85,9 @@ impl<'dir> File<'dir> {
         debug!("Statting file {:?}", &path);
         let metadata   = std::fs::symlink_metadata(&path)?;
         let is_all_all = false;
+        let extended_attributes = File::gather_extended_attributes(&path);
 
-        Ok(File { name, ext, path, metadata, parent_dir, is_all_all })
+        Ok(File { name, ext, path, metadata, parent_dir, is_all_all, extended_attributes })
     }
 
     pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
@@ -92,8 +98,9 @@ impl<'dir> File<'dir> {
         let metadata   = std::fs::symlink_metadata(&path)?;
         let is_all_all = true;
         let parent_dir = Some(parent_dir);
+        let extended_attributes = File::gather_extended_attributes(&path);
 
-        Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all })
+        Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, extended_attributes })
     }
 
     pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
@@ -103,8 +110,9 @@ impl<'dir> File<'dir> {
         let metadata   = std::fs::symlink_metadata(&path)?;
         let is_all_all = true;
         let parent_dir = Some(parent_dir);
+        let extended_attributes = File::gather_extended_attributes(&path);
 
-        Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all })
+        Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, extended_attributes })
     }
 
     /// A file’s name is derived from its string. This needs to handle directories
@@ -137,6 +145,21 @@ impl<'dir> File<'dir> {
             .to_ascii_lowercase())
     }
 
+    /// Read the extended attributes of a file path.
+    fn gather_extended_attributes(path: &Path) -> Vec<Attribute> {
+        if xattr::ENABLED {
+            match path.symlink_attributes() {
+                Ok(xattrs) => xattrs,
+                Err(e) => {
+                    error!("Error looking up extended attributes for {}: {}", path.display(), e);
+                    Vec::new()
+                }
+            }
+        } else {
+            Vec::new()
+        }
+    }
+
     /// Whether this file is a directory on the filesystem.
     pub fn is_directory(&self) -> bool {
         self.metadata.is_dir()
@@ -221,13 +244,13 @@ impl<'dir> File<'dir> {
             path.to_path_buf()
         }
         else if let Some(dir) = self.parent_dir {
-            dir.join(&*path)
+            dir.join(path)
         }
         else if let Some(parent) = self.path.parent() {
-            parent.join(&*path)
+            parent.join(path)
         }
         else {
-            self.path.join(&*path)
+            self.path.join(path)
         }
     }
 
@@ -261,7 +284,8 @@ impl<'dir> File<'dir> {
             Ok(metadata) => {
                 let ext  = File::ext(&path);
                 let name = File::filename(&path);
-                let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false };
+                let extended_attributes = File::gather_extended_attributes(&absolute_path);
+                let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false, extended_attributes };
                 FileTarget::Ok(Box::new(file))
             }
             Err(e) => {
@@ -375,7 +399,7 @@ impl<'dir> File<'dir> {
                 nanosec -= 1_000_000_000;
             }
 
-            let duration = Duration::new(sec.abs() as u64, nanosec.abs() as u32);
+            let duration = Duration::new(sec.unsigned_abs(), nanosec.unsigned_abs() as u32);
             Some(UNIX_EPOCH - duration)
         }
         else {
@@ -501,6 +525,16 @@ impl<'dir> File<'dir> {
     pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
         choices.contains(&&self.name[..])
     }
+
+    /// This file’s security context field.
+    pub fn security_context(&self) -> f::SecurityContext<'_> {
+        let context = match &self.extended_attributes.iter().find(|a| a.name == "security.selinux") {
+            Some(attr) => f::SecurityContextType::SELinux(&attr.value),
+            None       => f::SecurityContextType::None
+        };
+
+        f::SecurityContext { context }
+    }
 }
 
 

+ 14 - 14
src/fs/filter.rs

@@ -23,7 +23,7 @@ use crate::fs::File;
 /// The filter also governs sorting the list. After being filtered, pairs of
 /// files are compared and sorted based on the result, with the sort field
 /// performing the comparison.
-#[derive(PartialEq, Debug, Clone)]
+#[derive(PartialEq, Eq, Debug, Clone)]
 pub struct FileFilter {
 
     /// Whether directories should be listed first, and other types of file
@@ -89,7 +89,7 @@ impl FileFilter {
     }
 
     /// Sort the files in the given vector based on the sort field option.
-    pub fn sort_files<'a, F>(&self, files: &mut Vec<F>)
+    pub fn sort_files<'a, F>(&self, files: &mut [F])
     where F: AsRef<File<'a>>
     {
         files.sort_by(|a, b| {
@@ -113,7 +113,7 @@ impl FileFilter {
 
 
 /// User-supplied field to sort by.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum SortField {
 
     /// Don’t apply any sorting. This is usually used as an optimisation in
@@ -194,7 +194,7 @@ pub enum SortField {
 /// lowercase letters because it takes the difference between the two cases
 /// into account? I gave up and just named these two variants after the
 /// effects they have.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum SortCase {
 
     /// Sort files case-sensitively with uppercase first, with ‘A’ coming
@@ -271,7 +271,7 @@ impl SortField {
 /// The **ignore patterns** are a list of globs that are tested against
 /// each filename, and if any of them match, that file isn’t displayed.
 /// This lets a user hide, say, text files by ignoring `*.txt`.
-#[derive(PartialEq, Default, Debug, Clone)]
+#[derive(PartialEq, Eq, Default, Debug, Clone)]
 pub struct IgnorePatterns {
     patterns: Vec<glob::Pattern>,
 }
@@ -327,7 +327,7 @@ impl IgnorePatterns {
 
 
 /// Whether to ignore or display files that Git would ignore.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum GitIgnore {
 
     /// Ignore files that Git would ignore.
@@ -346,31 +346,31 @@ mod test_ignores {
     #[test]
     fn empty_matches_nothing() {
         let pats = IgnorePatterns::empty();
-        assert_eq!(false, pats.is_ignored("nothing"));
-        assert_eq!(false, pats.is_ignored("test.mp3"));
+        assert!(!pats.is_ignored("nothing"));
+        assert!(!pats.is_ignored("test.mp3"));
     }
 
     #[test]
     fn ignores_a_glob() {
         let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "*.mp3" ]);
         assert!(fails.is_empty());
-        assert_eq!(false, pats.is_ignored("nothing"));
-        assert_eq!(true,  pats.is_ignored("test.mp3"));
+        assert!(!pats.is_ignored("nothing"));
+        assert!(pats.is_ignored("test.mp3"));
     }
 
     #[test]
     fn ignores_an_exact_filename() {
         let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing" ]);
         assert!(fails.is_empty());
-        assert_eq!(true,  pats.is_ignored("nothing"));
-        assert_eq!(false, pats.is_ignored("test.mp3"));
+        assert!(pats.is_ignored("nothing"));
+        assert!(!pats.is_ignored("test.mp3"));
     }
 
     #[test]
     fn ignores_both() {
         let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing", "*.mp3" ]);
         assert!(fails.is_empty());
-        assert_eq!(true, pats.is_ignored("nothing"));
-        assert_eq!(true, pats.is_ignored("test.mp3"));
+        assert!(pats.is_ignored("nothing"));
+        assert!(pats.is_ignored("test.mp3"));
     }
 }

+ 1 - 1
src/info/filetype.rs

@@ -11,7 +11,7 @@ use crate::output::icons::FileIcon;
 use crate::theme::FileColours;
 
 
-#[derive(Debug, Default, PartialEq)]
+#[derive(Debug, Default, PartialEq, Eq)]
 pub struct FileExtensions;
 
 impl FileExtensions {

+ 1 - 1
src/main.rs

@@ -62,7 +62,7 @@ fn main() {
     }
 
     let args: Vec<_> = env::args_os().skip(1).collect();
-    match Options::parse(args.iter().map(|e| e.as_ref()), &LiveVars) {
+    match Options::parse(args.iter().map(std::convert::AsRef::as_ref), &LiveVars) {
         OptionsResult::Ok(options, mut input_paths) => {
 
             // List the current directory by default.

+ 4 - 4
src/options/error.rs

@@ -7,7 +7,7 @@ use crate::options::parser::{Arg, Flag, ParseError};
 
 
 /// Something wrong with the combination of options the user has picked.
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub enum OptionsError {
 
     /// There was an error (from `getopts`) parsing the arguments.
@@ -44,13 +44,13 @@ pub enum OptionsError {
 }
 
 /// The source of a string that failed to be parsed as a number.
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub enum NumberSource {
 
     /// It came... from a command-line argument!
     Arg(&'static Arg),
 
-    /// It came... from the enviroment!
+    /// It came... from the environment!
     Env(&'static str),
 }
 
@@ -119,7 +119,7 @@ impl OptionsError {
 
 
 /// A list of legal choices for an argument-taking option.
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub struct Choices(pub &'static [&'static str]);
 
 impl fmt::Display for Choices {

+ 0 - 1
src/options/filter.rs

@@ -295,7 +295,6 @@ mod test {
     mod ignore_patterns {
         use super::*;
         use std::iter::FromIterator;
-        use glob;
 
         fn pat(string: &'static str) -> glob::Pattern {
             glob::Pattern::new(string).unwrap()

+ 7 - 4
src/options/flags.rs

@@ -62,9 +62,12 @@ pub static NO_TIME: Arg = Arg { short: None, long: "no-time", takes_value: Takes
 pub static NO_ICONS: Arg = Arg { short: None, long: "no-icons", takes_value: TakesValue::Forbidden };
 
 // optional feature options
-pub static GIT:       Arg = Arg { short: None,       long: "git",               takes_value: TakesValue::Forbidden };
-pub static EXTENDED:  Arg = Arg { short: Some(b'@'), long: "extended",          takes_value: TakesValue::Forbidden };
-pub static OCTAL:     Arg = Arg { short: Some(b'o'), long: "octal-permissions", takes_value: TakesValue::Forbidden };
+pub static GIT:               Arg = Arg { short: None,       long: "git",                  takes_value: TakesValue::Forbidden };
+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 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 ALL_ARGS: Args = Args(&[
@@ -80,5 +83,5 @@ pub static ALL_ARGS: Args = Args(&[
     &BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE,
     &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &NO_ICONS,
 
-    &GIT, &EXTENDED, &OCTAL
+    &GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT, &EXTENDED, &OCTAL, &SECURITY_CONTEXT
 ]);

+ 5 - 4
src/options/help.rs

@@ -62,14 +62,14 @@ LONG VIEW OPTIONS
   --no-time                suppress the time field";
 
 static GIT_FILTER_HELP: &str = "  --git-ignore               ignore files mentioned in '.gitignore'";
-static GIT_VIEW_HELP:   &str = "  --git                    list each file's Git status, if tracked or ignored";
-static EXTENDED_HELP:   &str = "  -@, --extended           list each file's extended attributes and sizes";
-
+static GIT_VIEW_HELP:   &str = "  --git                list each file's Git status, if tracked or ignored";
+static EXTENDED_HELP:   &str = "  -@, --extended       list each file's extended attributes and sizes";
+static SECATTR_HELP:    &str = "  -Z, --context        list each file's security context";
 
 /// All the information needed to display the help text, which depends
 /// on which features are enabled and whether the user only wants to
 /// see one section’s help.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub struct HelpString;
 
 impl HelpString {
@@ -110,6 +110,7 @@ impl fmt::Display for HelpString {
 
         if xattr::ENABLED {
             write!(f, "\n{}", EXTENDED_HELP)?;
+            write!(f, "\n{}", SECATTR_HELP)?;
         }
 
         writeln!(f)

+ 3 - 3
src/options/mod.rs

@@ -216,7 +216,7 @@ pub mod test {
     use crate::options::parser::{Arg, MatchedFlags};
     use std::ffi::OsStr;
 
-    #[derive(PartialEq, Debug)]
+    #[derive(PartialEq, Eq, Debug)]
     pub enum Strictnesses {
         Last,
         Complain,
@@ -228,14 +228,14 @@ pub mod test {
     /// both, then both should resolve to the same result.
     ///
     /// It returns a vector with one or two elements in.
-    /// These elements can then be tested with assert_eq or what have you.
+    /// These elements can then be tested with `assert_eq` or what have you.
     pub fn parse_for_test<T, F>(inputs: &[&str], args: &'static [&'static Arg], strictnesses: Strictnesses, get: F) -> Vec<T>
     where F: Fn(&MatchedFlags<'_>) -> T
     {
         use self::Strictnesses::*;
         use crate::options::parser::{Args, Strictness};
 
-        let bits = inputs.into_iter().map(OsStr::new).collect::<Vec<_>>();
+        let bits = inputs.iter().map(OsStr::new).collect::<Vec<_>>();
         let mut result = Vec::new();
 
         if strictnesses == Last || strictnesses == Both {

+ 9 - 9
src/options/parser.rs

@@ -52,7 +52,7 @@ pub type Values = &'static [&'static str];
 
 /// A **flag** is either of the two argument types, because they have to
 /// be in the same array together.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum Flag {
     Short(ShortArg),
     Long(LongArg),
@@ -77,7 +77,7 @@ impl fmt::Display for Flag {
 }
 
 /// Whether redundant arguments should be considered a problem.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum Strictness {
 
     /// Throw an error when an argument doesn’t do anything, either because
@@ -91,7 +91,7 @@ pub enum Strictness {
 
 /// Whether a flag takes a value. This is applicable to both long and short
 /// arguments.
-#[derive(Copy, Clone, PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum TakesValue {
 
     /// This flag has to be followed by a value.
@@ -108,7 +108,7 @@ pub enum TakesValue {
 
 
 /// An **argument** can be matched by one of the user’s input strings.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub struct Arg {
 
     /// The short argument that matches it, if any.
@@ -136,7 +136,7 @@ impl fmt::Display for Arg {
 
 
 /// Literally just several args.
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub struct Args(pub &'static [&'static Arg]);
 
 impl Args {
@@ -340,7 +340,7 @@ impl Args {
 
 
 /// The **matches** are the result of parsing the user’s command-line strings.
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub struct Matches<'args> {
 
     /// The flags that were parsed from the user’s input.
@@ -351,7 +351,7 @@ pub struct Matches<'args> {
     pub frees: Vec<&'args OsStr>,
 }
 
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub struct MatchedFlags<'args> {
 
     /// The individual flags from the user’s input, in the order they were
@@ -462,7 +462,7 @@ impl<'a> MatchedFlags<'a> {
 
 /// A problem with the user’s input that meant it couldn’t be parsed into a
 /// coherent list of arguments.
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub enum ParseError {
 
     /// A flag that has to take a value was not given one.
@@ -743,6 +743,6 @@ mod matches_test {
     fn no_count() {
         let flags = MatchedFlags { flags: Vec::new(), strictness: Strictness::UseLastArguments };
 
-        assert_eq!(flags.has(&COUNT).unwrap(), false);
+        assert!(!flags.has(&COUNT).unwrap());
     }
 }

+ 4 - 4
src/options/theme.rs

@@ -130,18 +130,18 @@ mod terminal_test {
 
     impl MockVars {
         fn empty() -> MockVars {
-            return MockVars {
+            MockVars {
                 ls: "",
                 exa: "",
                 no_color: "",
-            };
+            }
         }
         fn with_no_color() -> MockVars {
-            return MockVars {
+            MockVars {
                 ls: "",
                 exa: "",
                 no_color: "true",
-            };
+            }
         }
     }
 

+ 1 - 1
src/options/version.rs

@@ -8,7 +8,7 @@ use crate::options::flags;
 use crate::options::parser::MatchedFlags;
 
 
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub struct VersionString;
 // There were options here once, but there aren’t anymore!
 

+ 14 - 8
src/options/view.rs

@@ -117,6 +117,7 @@ impl details::Options {
             table: None,
             header: false,
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
+            secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
         };
 
         Ok(details)
@@ -136,6 +137,7 @@ impl details::Options {
             table: Some(TableOptions::deduce(matches, vars)?),
             header: matches.has(&flags::HEADER)?,
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
+            secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
         })
     }
 }
@@ -199,19 +201,23 @@ impl TableOptions {
 impl Columns {
     fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
         let time_types = TimeTypes::deduce(matches)?;
+
         let git = matches.has(&flags::GIT)?;
+        let subdir_git_repos = matches.has(&flags::GIT_REPOS)?;
+        let subdir_git_repos_no_stat = !subdir_git_repos && matches.has(&flags::GIT_REPOS_NO_STAT)?;
 
-        let blocks = matches.has(&flags::BLOCKS)?;
-        let group  = matches.has(&flags::GROUP)?;
-        let inode  = matches.has(&flags::INODE)?;
-        let links  = matches.has(&flags::LINKS)?;
-        let octal  = matches.has(&flags::OCTAL)?;
+        let blocks           = matches.has(&flags::BLOCKS)?;
+        let group            = matches.has(&flags::GROUP)?;
+        let inode            = matches.has(&flags::INODE)?;
+        let links            = matches.has(&flags::LINKS)?;
+        let octal            = matches.has(&flags::OCTAL)?;
+        let security_context = xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?;
 
         let permissions = ! matches.has(&flags::NO_PERMISSIONS)?;
         let filesize =    ! matches.has(&flags::NO_FILESIZE)?;
         let user =        ! matches.has(&flags::NO_USER)?;
 
-        Ok(Self { time_types, inode, links, blocks, group, git, octal, permissions, filesize, user })
+        Ok(Self { time_types, inode, links, blocks, group, git, subdir_git_repos, subdir_git_repos_no_stat, octal, security_context, permissions, filesize, user })
     }
 }
 
@@ -378,7 +384,7 @@ mod test {
 
         ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => err $result:expr) => {
             /// Special macro for testing Err results.
-            /// This is needed because sometimes the Ok type doesn’t implement PartialEq.
+            /// This is needed because sometimes the Ok type doesn’t implement `PartialEq`.
             #[test]
             fn $name() {
                 for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
@@ -389,7 +395,7 @@ mod test {
 
         ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => like $pat:pat) => {
             /// More general macro for testing against a pattern.
-            /// Instead of using PartialEq, this just tests if it matches a pat.
+            /// Instead of using `PartialEq`, this just tests if it matches a pat.
             #[test]
             fn $name() {
                 for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {

+ 2 - 4
src/output/cell.rs

@@ -77,11 +77,9 @@ impl TextCell {
     ///
     /// This method allocates a `String` to hold the spaces.
     pub fn add_spaces(&mut self, count: usize) {
-        use std::iter::repeat;
-
         (*self.width) += count;
 
-        let spaces: String = repeat(' ').take(count).collect();
+        let spaces: String = " ".repeat(count);
         self.contents.0.push(Style::default().paint(spaces));
     }
 
@@ -193,7 +191,7 @@ impl TextCellContents {
 ///
 /// It has `From` impls that convert an input string or fixed with to values
 /// of this type, and will `Deref` to the contained `usize` value.
-#[derive(PartialEq, Debug, Clone, Copy, Default)]
+#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
 pub struct DisplayWidth(usize);
 
 impl<'a> From<&'a str> for DisplayWidth {

+ 30 - 30
src/output/details.rs

@@ -71,7 +71,8 @@ use scoped_threadpool::Pool;
 use crate::fs::{Dir, File};
 use crate::fs::dir_action::RecurseOptions;
 use crate::fs::feature::git::GitCache;
-use crate::fs::feature::xattr::{Attribute, FileAttributes};
+use crate::fs::feature::xattr::Attribute;
+use crate::fs::fields::SecurityContextType;
 use crate::fs::filter::FileFilter;
 use crate::output::cell::TextCell;
 use crate::output::file_name::Options as FileStyle;
@@ -91,7 +92,7 @@ use crate::theme::Theme;
 ///
 /// Almost all the heavy lifting is done in a Table object, which handles the
 /// columns for each row.
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub struct Options {
 
     /// Options specific to drawing a table.
@@ -105,6 +106,9 @@ pub struct Options {
 
     /// Whether to show each file’s extended attributes.
     pub xattr: bool,
+
+    /// Whether to show each file's security attribute.
+    pub secattr: bool,
 }
 
 
@@ -132,7 +136,7 @@ pub struct Render<'a> {
 
 struct Egg<'a> {
     table_row: Option<TableRow>,
-    xattrs:    Vec<Attribute>,
+    xattrs:    &'a [Attribute],
     errors:    Vec<(io::Error, Option<PathBuf>)>,
     dir:       Option<Dir>,
     file:      &'a File<'a>,
@@ -161,7 +165,7 @@ impl<'a> Render<'a> {
                 (None,    _)        => {/* Keep Git how it is */},
             }
 
-            let mut table = Table::new(table, self.git, &self.theme);
+            let mut table = Table::new(table, self.git, self.theme);
 
             if self.opts.header {
                 let header = table.header_row();
@@ -189,11 +193,22 @@ impl<'a> Render<'a> {
         Ok(())
     }
 
+    /// Whether to show the extended attribute hint
+    pub fn show_xattr_hint(&self, file: &File<'_>) -> bool {
+        // Do not show the hint '@' if the only extended attribute is the security
+        // attribute and the security attribute column is active.
+        let xattr_count = file.extended_attributes.len();
+        let selinux_ctx_shown = self.opts.secattr && match file.security_context().context {
+            SecurityContextType::SELinux(_) => true,
+            SecurityContextType::None       => false,
+        };
+        xattr_count > 1 || (xattr_count == 1 && !selinux_ctx_shown)
+    }
+
     /// Adds files to the table, possibly recursively. This is easily
     /// parallelisable, and uses a pool of threads.
     fn add_files_to_table<'dir>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], depth: TreeDepth) {
         use std::sync::{Arc, Mutex};
-        use log::*;
         use crate::fs::feature::xattr;
 
         let mut file_eggs = (0..src.len()).map(|_| MaybeUninit::uninit()).collect::<Vec<_>>();
@@ -207,7 +222,6 @@ impl<'a> Render<'a> {
 
                 scoped.execute(move || {
                     let mut errors = Vec::new();
-                    let mut xattrs = Vec::new();
 
                     // There are three “levels” of extended attribute support:
                     //
@@ -216,7 +230,7 @@ impl<'a> Render<'a> {
                     // 2. If the feature is enabled and the --extended flag
                     //    has been specified, then display an @ in the
                     //    permissions column for files with attributes, the
-                    //    names of all attributes and their lengths, and any
+                    //    names of all attributes and their values, and any
                     //    errors encountered when getting them.
                     // 3. If the --extended flag *hasn’t* been specified, then
                     //    display the @, but don’t display anything else.
@@ -231,28 +245,14 @@ impl<'a> Render<'a> {
                     // printed unless the user passes --extended to signify
                     // that they want to see them.
 
-                    if xattr::ENABLED {
-                        match file.path.attributes() {
-                            Ok(xs) => {
-                                xattrs.extend(xs);
-                            }
-                            Err(e) => {
-                                if self.opts.xattr {
-                                    errors.push((e, None));
-                                }
-                                else {
-                                    error!("Error looking up xattr for {:?}: {:#?}", file.path, e);
-                                }
-                            }
-                        }
-                    }
+                    let xattrs: &[Attribute] = if xattr::ENABLED && self.opts.xattr {
+                        &file.extended_attributes
+                    } else {
+                        &[]
+                    };
 
                     let table_row = table.as_ref()
-                                         .map(|t| t.row_for_file(file, ! xattrs.is_empty()));
-
-                    if ! self.opts.xattr {
-                        xattrs.clear();
-                    }
+                                         .map(|t| t.row_for_file(file, self.show_xattr_hint(file)));
 
                     let mut dir = None;
                     if let Some(r) = self.recurse {
@@ -315,7 +315,7 @@ impl<'a> Render<'a> {
 
                 if ! files.is_empty() {
                     for xattr in egg.xattrs {
-                        rows.push(self.render_xattr(&xattr, TreeParams::new(depth.deeper(), false)));
+                        rows.push(self.render_xattr(xattr, TreeParams::new(depth.deeper(), false)));
                     }
 
                     for (error, path) in errors {
@@ -330,7 +330,7 @@ impl<'a> Render<'a> {
             let count = egg.xattrs.len();
             for (index, xattr) in egg.xattrs.into_iter().enumerate() {
                 let params = TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1);
-                let r = self.render_xattr(&xattr, params);
+                let r = self.render_xattr(xattr, params);
                 rows.push(r);
             }
 
@@ -367,7 +367,7 @@ impl<'a> Render<'a> {
     }
 
     fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
-        let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{} (len {})", xattr.name, xattr.size));
+        let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{}=\"{}\"", xattr.name, xattr.value));
         Row { cells: None, name, tree }
     }
 

+ 2 - 2
src/output/file_name.rs

@@ -54,7 +54,7 @@ enum LinkStyle {
 
 
 /// Whether to append file class characters to the file names.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum Classify {
 
     /// Just display the file names, without any characters.
@@ -73,7 +73,7 @@ impl Default for Classify {
 
 
 /// Whether and how to show icons.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum ShowIcons {
 
     /// Don’t show icons at all.

+ 1 - 1
src/output/grid.rs

@@ -8,7 +8,7 @@ use crate::output::file_name::Options as FileStyle;
 use crate::theme::Theme;
 
 
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub struct Options {
     pub across: bool,
 }

+ 4 - 13
src/output/grid_details.rs

@@ -7,7 +7,6 @@ use term_grid as grid;
 
 use crate::fs::{Dir, File};
 use crate::fs::feature::git::GitCache;
-use crate::fs::feature::xattr::FileAttributes;
 use crate::fs::filter::FileFilter;
 use crate::output::cell::TextCell;
 use crate::output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
@@ -18,7 +17,7 @@ use crate::output::tree::{TreeParams, TreeDepth};
 use crate::theme::Theme;
 
 
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub struct Options {
     pub grid: GridOptions,
     pub details: DetailsOptions,
@@ -39,7 +38,7 @@ impl Options {
 /// small directory of four files in four columns, the files just look spaced
 /// out and it’s harder to see what’s going on. So it can be enabled just for
 /// larger directory listings.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum RowThreshold {
 
     /// Only use grid-details view if it would result in at least this many
@@ -150,7 +149,7 @@ impl<'a> Render<'a> {
         let (first_table, _) = self.make_table(options, &drender);
 
         let rows = self.files.iter()
-                       .map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
+                       .map(|file| first_table.row_for_file(file, drender.show_xattr_hint(file)))
                        .collect::<Vec<_>>();
 
         let file_names = self.files.iter()
@@ -202,7 +201,7 @@ impl<'a> Render<'a> {
             (None,    _)        => {/* Keep Git how it is */},
         }
 
-        let mut table = Table::new(options, self.git, &self.theme);
+        let mut table = Table::new(options, self.git, self.theme);
         let mut rows = Vec::new();
 
         if self.details.header {
@@ -299,11 +298,3 @@ fn divide_rounding_up(a: usize, b: usize) -> usize {
 
     result
 }
-
-
-fn file_has_xattrs(file: &File<'_>) -> bool {
-    match file.path.attributes() {
-        Ok(attrs)  => ! attrs.is_empty(),
-        Err(_)     => false,
-    }
-}

+ 2 - 2
src/output/mod.rs

@@ -26,7 +26,7 @@ pub struct View {
 
 
 /// The **mode** is the “type” of output.
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 #[allow(clippy::large_enum_variant)]
 pub enum Mode {
     Grid(grid::Options),
@@ -37,7 +37,7 @@ pub enum Mode {
 
 
 /// The width of the terminal requested by the user.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum TerminalWidth {
 
     /// The user requested this specific number of columns.

+ 2 - 2
src/output/render/blocks.rs

@@ -43,7 +43,7 @@ pub mod test {
         let blox = f::Blocks::None;
         let expected = TextCell::blank(Green.italic());
 
-        assert_eq!(expected, blox.render(&TestColours).into());
+        assert_eq!(expected, blox.render(&TestColours));
     }
 
 
@@ -52,6 +52,6 @@ pub mod test {
         let blox = f::Blocks::Some(3005);
         let expected = TextCell::paint_str(Red.blink(), "3005");
 
-        assert_eq!(expected, blox.render(&TestColours).into());
+        assert_eq!(expected, blox.render(&TestColours));
     }
 }

+ 28 - 3
src/output/render/git.rs

@@ -1,4 +1,4 @@
-use ansi_term::{ANSIString, Style};
+use ansi_term::{ANSIString, Style, Color};
 
 use crate::output::cell::{TextCell, DisplayWidth};
 use crate::fs::fields as f;
@@ -16,6 +16,31 @@ impl f::Git {
     }
 }
 
+impl f::SubdirGitRepo {
+    pub fn render(self) -> TextCell {
+        let style = Style::new();
+        let branch_style = match self.branch.as_deref(){
+            Some("master") => style.fg(Color::Green),
+            Some("main") => style.fg(Color::Green),
+            Some(_) => style.fg(Color::Fixed(208)),
+            _ => style,
+        };
+        
+        let branch = branch_style.paint(self.branch.unwrap_or(String::from("-")));
+
+        let s = match self.status {
+            f::SubdirGitRepoStatus::NoRepo => style.paint("- "),
+            f::SubdirGitRepoStatus::GitClean => style.fg(Color::Green).paint("| "),
+            f::SubdirGitRepoStatus::GitDirty => style.bold().fg(Color::Red).paint("- "),
+            f::SubdirGitRepoStatus::GitUnknown => style.paint("- "),
+        };
+
+        TextCell {
+            width: DisplayWidth::from(2 + branch.len()),
+            contents: vec![s,branch].into(),
+        }
+    }
+}
 
 impl f::GitStatus {
     fn render(self, colours: &dyn Colours) -> ANSIString<'static> {
@@ -85,7 +110,7 @@ pub mod test {
             ].into(),
         };
 
-        assert_eq!(expected, stati.render(&TestColours).into())
+        assert_eq!(expected, stati.render(&TestColours))
     }
 
 
@@ -104,6 +129,6 @@ pub mod test {
             ].into(),
         };
 
-        assert_eq!(expected, stati.render(&TestColours).into())
+        assert_eq!(expected, stati.render(&TestColours))
     }
 }

+ 2 - 2
src/output/render/inode.rs

@@ -21,8 +21,8 @@ pub mod test {
 
     #[test]
     fn blocklessness() {
-        let io = f::Inode(1414213);
+        let io = f::Inode(1_414_213);
         let expected = TextCell::paint_str(Cyan.underline(), "1414213");
-        assert_eq!(expected, io.render(Cyan.underline()).into());
+        assert_eq!(expected, io.render(Cyan.underline()));
     }
 }

+ 3 - 3
src/output/render/links.rs

@@ -52,7 +52,7 @@ pub mod test {
             contents: vec![ Blue.paint("1") ].into(),
         };
 
-        assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()).into());
+        assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()));
     }
 
     #[test]
@@ -67,7 +67,7 @@ pub mod test {
             contents: vec![ Blue.paint("3,005") ].into(),
         };
 
-        assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()).into());
+        assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()));
     }
 
     #[test]
@@ -82,6 +82,6 @@ pub mod test {
             contents: vec![ Blue.on(Red).paint("3,005") ].into(),
         };
 
-        assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()).into());
+        assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()));
     }
 }

+ 3 - 0
src/output/render/mod.rs

@@ -35,3 +35,6 @@ pub use self::users::Colours as UserColours;
 
 mod octal;
 // octal uses just one colour
+
+mod securityctx;
+pub use self::securityctx::Colours as SecurityCtxColours;

+ 7 - 7
src/output/render/octal.rs

@@ -6,7 +6,7 @@ use crate::output::cell::TextCell;
 
 impl f::OctalPermissions {
     fn bits_to_octal(r: bool, w: bool, x: bool) -> u8 {
-        (r as u8) * 4 + (w as u8) * 2 + (x as u8)
+        u8::from(r) * 4 + u8::from(w) * 2 + u8::from(x)
     }
 
     pub fn render(&self, style: Style) -> TextCell {
@@ -40,7 +40,7 @@ pub mod test {
         let octal = f::OctalPermissions{ permissions: bits };
 
         let expected = TextCell::paint_str(Purple.bold(), "0755");
-        assert_eq!(expected, octal.render(Purple.bold()).into());
+        assert_eq!(expected, octal.render(Purple.bold()));
     }
 
     #[test]
@@ -54,7 +54,7 @@ pub mod test {
         let octal = f::OctalPermissions{ permissions: bits };
 
         let expected = TextCell::paint_str(Purple.bold(), "0644");
-        assert_eq!(expected, octal.render(Purple.bold()).into());
+        assert_eq!(expected, octal.render(Purple.bold()));
     }
 
     #[test]
@@ -68,7 +68,7 @@ pub mod test {
         let octal = f::OctalPermissions{ permissions: bits };
 
         let expected = TextCell::paint_str(Purple.bold(), "0600");
-        assert_eq!(expected, octal.render(Purple.bold()).into());
+        assert_eq!(expected, octal.render(Purple.bold()));
     }
 
     #[test]
@@ -82,7 +82,7 @@ pub mod test {
         let octal = f::OctalPermissions{ permissions: bits };
 
         let expected = TextCell::paint_str(Purple.bold(), "4777");
-        assert_eq!(expected, octal.render(Purple.bold()).into());
+        assert_eq!(expected, octal.render(Purple.bold()));
 
     }
 
@@ -97,7 +97,7 @@ pub mod test {
         let octal = f::OctalPermissions{ permissions: bits };
 
         let expected = TextCell::paint_str(Purple.bold(), "2777");
-        assert_eq!(expected, octal.render(Purple.bold()).into());
+        assert_eq!(expected, octal.render(Purple.bold()));
     }
 
     #[test]
@@ -111,6 +111,6 @@ pub mod test {
         let octal = f::OctalPermissions{ permissions: bits };
 
         let expected = TextCell::paint_str(Purple.bold(), "1777");
-        assert_eq!(expected, octal.render(Purple.bold()).into());
+        assert_eq!(expected, octal.render(Purple.bold()));
     }
 }

+ 1 - 0
src/output/render/permissions.rs

@@ -88,6 +88,7 @@ impl f::Permissions {
     }
 }
 
+#[cfg(windows)]
 impl f::Attributes {
     pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> Vec<ANSIString<'static>> {
         let bit = |bit, chr: &'static str, style: Style| {

+ 45 - 0
src/output/render/securityctx.rs

@@ -0,0 +1,45 @@
+use ansi_term::Style;
+
+use crate::fs::fields as f;
+use crate::output::cell::{TextCell, DisplayWidth};
+
+
+impl f::SecurityContext<'_> {
+    pub fn render<C: Colours>(&self, colours: &C) -> TextCell {
+        match &self.context {
+            f::SecurityContextType::None => {
+                TextCell::paint_str(colours.none(), "?")
+            }
+            f::SecurityContextType::SELinux(context) => {
+                let mut chars = Vec::with_capacity(7);
+
+                for (i, part) in context.split(':').enumerate() {
+                    let partcolour = match i {
+                        0 => colours.selinux_user(),
+                        1 => colours.selinux_role(),
+                        2 => colours.selinux_type(),
+                        _ => colours.selinux_range()
+                    };
+                    if i > 0 {
+                        chars.push(colours.selinux_colon().paint(":"));
+                    }
+                    chars.push(partcolour.paint(String::from(part)));
+                }
+
+                TextCell {
+                    contents: chars.into(),
+                    width: DisplayWidth::from(context.len())
+                }
+            }
+        }
+    }
+}
+
+pub trait Colours {
+    fn none(&self)          -> Style;
+    fn selinux_colon(&self) -> Style;
+    fn selinux_user(&self)  -> Style;
+    fn selinux_role(&self)  -> Style;
+    fn selinux_type(&self)  -> Style;
+    fn selinux_range(&self) -> Style;
+}

+ 1 - 1
src/output/render/size.rs

@@ -153,7 +153,7 @@ pub mod test {
 
     #[test]
     fn file_bytes() {
-        let directory = f::Size::Some(1048576);
+        let directory = f::Size::Some(1_048_576);
         let expected = TextCell {
             width: DisplayWidth::from(9),
             contents: vec![

+ 51 - 9
src/output/table.rs

@@ -21,7 +21,7 @@ use crate::theme::Theme;
 
 
 /// Options for displaying a table.
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub struct Options {
     pub size_format: SizeFormat,
     pub time_format: TimeFormat,
@@ -31,7 +31,7 @@ pub struct Options {
 
 /// Extra columns to display in the table.
 #[allow(clippy::struct_excessive_bools)]
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub struct Columns {
 
     /// At least one of these timestamps will be shown.
@@ -43,7 +43,10 @@ pub struct Columns {
     pub blocks: bool,
     pub group: bool,
     pub git: bool,
+    pub subdir_git_repos: bool,
+    pub subdir_git_repos_no_stat: bool,
     pub octal: bool,
+    pub security_context: bool,
 
     // Defaults to true:
     pub permissions: bool,
@@ -93,6 +96,10 @@ impl Columns {
             columns.push(Column::Group);
         }
 
+        if self.security_context {
+            columns.push(Column::SecurityContext);
+        }
+
         if self.time_types.modified {
             columns.push(Column::Timestamp(TimeType::Modified));
         }
@@ -113,6 +120,14 @@ impl Columns {
             columns.push(Column::GitStatus);
         }
 
+        if self.subdir_git_repos {
+            columns.push(Column::SubdirGitRepoStatus);
+        }
+
+        if self.subdir_git_repos_no_stat {
+            columns.push(Column::SubdirGitRepoNoStatus);
+        }
+
         columns
     }
 }
@@ -135,8 +150,12 @@ pub enum Column {
     #[cfg(unix)]
     Inode,
     GitStatus,
+    SubdirGitRepoStatus,
+    SubdirGitRepoNoStatus,
     #[cfg(unix)]
     Octal,
+    #[cfg(unix)]
+    SecurityContext,
 }
 
 /// Each column can pick its own **Alignment**. Usually, numbers are
@@ -192,16 +211,20 @@ impl Column {
             #[cfg(unix)]
             Self::Inode         => "inode",
             Self::GitStatus     => "Git",
+            Self::SubdirGitRepoStatus => "Repo",
+            Self::SubdirGitRepoNoStatus => "Repo",
             #[cfg(unix)]
             Self::Octal         => "Octal",
+            #[cfg(unix)]
+            Self::SecurityContext => "Security Context",
         }
     }
 }
 
 
 /// Formatting options for file sizes.
-#[allow(clippy::pub_enum_variant_names)]
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[allow(clippy::enum_variant_names)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum SizeFormat {
 
     /// Format the file size using **decimal** prefixes, such as “kilo”,
@@ -217,7 +240,7 @@ pub enum SizeFormat {
 }
 
 /// Formatting options for user and group.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum UserFormat {
     /// The UID / GID
     Numeric,
@@ -234,7 +257,7 @@ impl Default for SizeFormat {
 
 /// The types of a file’s time fields. These three fields are standard
 /// across most (all?) operating systems.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum TimeType {
 
     /// The file’s modified time (`st_mtime`).
@@ -269,7 +292,7 @@ impl TimeType {
 ///
 /// There should always be at least one of these — there’s no way to disable
 /// the time columns entirely (yet).
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 #[allow(clippy::struct_excessive_bools)]
 pub struct TimeTypes {
     pub modified: bool,
@@ -323,7 +346,7 @@ impl Environment {
                 Some(t)
             }
             Err(ref e) => {
-                println!("Unable to determine time zone: {}", e);
+                eprintln!("Unable to determine time zone: {e}");
                 None
             }
         };
@@ -347,7 +370,7 @@ fn determine_time_zone() -> TZResult<TimeZone> {
             } else {
                 format!("/usr/share/zoneinfo/{}", {
                     if file.starts_with(':') {
-                        file.replacen(":", "", 1)
+                        file.replacen(':', "", 1)
                     } else {
                         file
                     }
@@ -493,9 +516,19 @@ impl<'a, 'f> Table<'a> {
             Column::Group => {
                 file.group().render(self.theme, &*self.env.lock_users(), self.user_format)
             }
+            #[cfg(unix)]
+            Column::SecurityContext => {
+                file.security_context().render(self.theme)
+            }
             Column::GitStatus => {
                 self.git_status(file).render(self.theme)
             }
+            Column::SubdirGitRepoStatus => {
+                self.subdir_git_repo(file, true).render()
+            }
+            Column::SubdirGitRepoNoStatus => {
+                self.subdir_git_repo(file, false).render()
+            }
             #[cfg(unix)]
             Column::Octal => {
                 self.octal_permissions(file).render(self.theme.ui.octal)
@@ -524,6 +557,15 @@ impl<'a, 'f> Table<'a> {
             .unwrap_or_default()
     }
 
+    fn subdir_git_repo(&self, file: &File<'_>, status : bool) -> f::SubdirGitRepo {
+        debug!("Getting subdir repo status for path {:?}", file.path);
+
+        if file.is_directory(){
+            return f::SubdirGitRepo::from_path(&file.path, status);
+        }
+        f::SubdirGitRepo::default()
+    }
+
     pub fn render(&self, row: Row) -> TextCell {
         let mut cell = TextCell::default();
 

+ 2 - 2
src/output/time.rs

@@ -25,7 +25,7 @@ use unicode_width::UnicodeWidthStr;
 ///
 /// Currently exa does not support *custom* styles, where the user enters a
 /// format string in an environment variable or something. Just these four.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum TimeFormat {
 
     /// The **default format** uses the user’s locale to print month names,
@@ -87,7 +87,7 @@ fn default_zoned(time: SystemTime, zone: &TimeZone) -> String {
 }
 
 fn get_dateformat(date: &LocalDateTime) -> &'static DateFormat<'static> {
-    match (is_recent(&date), *MAXIMUM_MONTH_WIDTH) {
+    match (is_recent(date), *MAXIMUM_MONTH_WIDTH) {
         (true, 4)   => &FOUR_WIDE_DATE_TIME,
         (true, 5)   => &FIVE_WIDE_DATE_TIME,
         (true, _)   => &OTHER_WIDE_DATE_TIME,

+ 6 - 6
src/output/tree.rs

@@ -39,7 +39,7 @@
 //! each directory)
 
 
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum TreePart {
 
     /// Rightmost column, *not* the last in the directory.
@@ -253,19 +253,19 @@ mod iter_test {
     #[test]
     fn test_iteration() {
         let foos = &[ "first", "middle", "last" ];
-        let mut iter = TreeDepth::root().iterate_over(foos.into_iter());
+        let mut iter = TreeDepth::root().iterate_over(foos.iter());
 
         let next = iter.next().unwrap();
         assert_eq!(&"first", next.1);
-        assert_eq!(false, next.0.last);
+        assert!(!next.0.last);
 
         let next = iter.next().unwrap();
         assert_eq!(&"middle", next.1);
-        assert_eq!(false, next.0.last);
+        assert!(!next.0.last);
 
         let next = iter.next().unwrap();
         assert_eq!(&"last", next.1);
-        assert_eq!(true, next.0.last);
+        assert!(next.0.last);
 
         assert!(iter.next().is_none());
     }
@@ -273,7 +273,7 @@ mod iter_test {
     #[test]
     fn test_empty() {
         let nothing: &[usize] = &[];
-        let mut iter = TreeDepth::root().iterate_over(nothing.into_iter());
+        let mut iter = TreeDepth::root().iterate_over(nothing.iter());
         assert!(iter.next().is_none());
     }
 }

+ 11 - 0
src/theme/default_theme.rs

@@ -66,6 +66,17 @@ impl UiStyles {
                 conflicted:  Red.normal(),
             },
 
+            security_context: SecurityContext {
+                none:       Style::default(),
+                selinux: SELinuxContext {
+                    colon: Style::default().dimmed(),
+                    user:  Blue.normal(),
+                    role:  Green.normal(),
+                    typ:   Yellow.normal(),
+                    range: Cyan.normal(),
+                },
+            },
+
             punctuation:  Fixed(244).normal(),
             date:         Blue.normal(),
             inode:        Purple.normal(),

+ 23 - 14
src/theme/mod.rs

@@ -14,7 +14,7 @@ pub use self::lsc::LSColors;
 mod default_theme;
 
 
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Eq, Debug)]
 pub struct Options {
 
     pub use_colours: UseColours,
@@ -31,7 +31,7 @@ pub struct Options {
 /// Turning them on when output is going to, say, a pipe, would make programs
 /// such as `grep` or `more` not work properly. So the `Automatic` mode does
 /// this check and only displays colours when they can be truly appreciated.
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum UseColours {
 
     /// Display them even when output isn’t going to a terminal.
@@ -44,13 +44,13 @@ pub enum UseColours {
     Never,
 }
 
-#[derive(PartialEq, Debug, Copy, Clone)]
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
 pub enum ColourScale {
     Fixed,
     Gradient,
 }
 
-#[derive(PartialEq, Debug, Default)]
+#[derive(PartialEq, Eq, Debug, Default)]
 pub struct Definitions {
     pub ls: Option<String>,
     pub exa: Option<String>,
@@ -262,11 +262,11 @@ impl render::SizeColours for Theme {
         use number_prefix::Prefix::*;
 
         match prefix {
-            None                    => self.ui.size.number_byte,
-            Some(Kilo) | Some(Kibi) => self.ui.size.number_kilo,
-            Some(Mega) | Some(Mebi) => self.ui.size.number_mega,
-            Some(Giga) | Some(Gibi) => self.ui.size.number_giga,
-            Some(_)                 => self.ui.size.number_huge,
+            Some(Kilo | Kibi) => self.ui.size.number_kilo,
+            Some(Mega | Mebi) => self.ui.size.number_mega,
+            Some(Giga | Gibi) => self.ui.size.number_giga,
+            Some(_)           => self.ui.size.number_huge,
+            None              => self.ui.size.number_byte,
         }
     }
 
@@ -274,11 +274,11 @@ impl render::SizeColours for Theme {
         use number_prefix::Prefix::*;
 
         match prefix {
-            None                    => self.ui.size.unit_byte,
-            Some(Kilo) | Some(Kibi) => self.ui.size.unit_kilo,
-            Some(Mega) | Some(Mebi) => self.ui.size.unit_mega,
-            Some(Giga) | Some(Gibi) => self.ui.size.unit_giga,
-            Some(_)                 => self.ui.size.unit_huge,
+            Some(Kilo | Kibi) => self.ui.size.unit_kilo,
+            Some(Mega | Mebi) => self.ui.size.unit_mega,
+            Some(Giga | Gibi) => self.ui.size.unit_giga,
+            Some(_)           => self.ui.size.unit_huge,
+            None              => self.ui.size.unit_byte,
         }
     }
 
@@ -308,6 +308,15 @@ impl FileNameColours for Theme {
     }
 }
 
+impl render::SecurityCtxColours for Theme {
+    fn none(&self)          -> Style { self.ui.security_context.none }
+    fn selinux_colon(&self) -> Style { self.ui.security_context.selinux.colon }
+    fn selinux_user(&self)  -> Style { self.ui.security_context.selinux.user }
+    fn selinux_role(&self)  -> Style { self.ui.security_context.selinux.role }
+    fn selinux_type(&self)  -> Style { self.ui.security_context.selinux.typ }
+    fn selinux_range(&self) -> Style { self.ui.security_context.selinux.range }
+}
+
 
 /// Some of the styles are **overlays**: although they have the same attribute
 /// set as regular styles (foreground and background colours, bold, underline,

+ 22 - 6
src/theme/ui_styles.rs

@@ -7,12 +7,13 @@ use crate::theme::lsc::Pair;
 pub struct UiStyles {
     pub colourful: bool,
 
-    pub filekinds:  FileKinds,
-    pub perms:      Permissions,
-    pub size:       Size,
-    pub users:      Users,
-    pub links:      Links,
-    pub git:        Git,
+    pub filekinds:        FileKinds,
+    pub perms:            Permissions,
+    pub size:             Size,
+    pub users:            Users,
+    pub links:            Links,
+    pub git:              Git,
+    pub security_context: SecurityContext,
 
     pub punctuation:  Style,
     pub date:         Style,
@@ -104,6 +105,21 @@ pub struct Git {
     pub conflicted: Style,
 }
 
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct SELinuxContext {
+    pub colon: Style,
+    pub user:  Style,
+    pub role:  Style,
+    pub typ:   Style,
+    pub range: Style,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct SecurityContext {
+    pub none:    Style,
+    pub selinux: SELinuxContext,
+}
+
 impl UiStyles {
     pub fn plain() -> Self {
         Self::default()