Merge branch 'main' into pr-1061

Signed-off-by: Christina Sørensen <christina@cafkafk.com>
This commit is contained in:
Christina Sørensen 2023-07-30 06:01:15 +02:00
commit 24655dfea5
No known key found for this signature in database
GPG key ID: CDDC792F655251ED
46 changed files with 1167 additions and 461 deletions

1
.github/FUNDING.yml vendored
View file

@ -1 +0,0 @@
github: ogham

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
# Nix Flake stuff
result
# Rust stuff
target

23
Cargo.lock generated
View file

@ -31,9 +31,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cc"
version = "1.0.67"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
dependencies = [
"jobserver",
]
@ -58,7 +58,7 @@ dependencies = [
]
[[package]]
name = "exa"
name = "eza"
version = "0.10.1"
dependencies = [
"ansi_term",
@ -93,9 +93,9 @@ dependencies = [
[[package]]
name = "git2"
version = "0.13.20"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9831e983241f8c5591ed53f17d874833e2fa82cac2625f3888c50cbfe136cba"
checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
dependencies = [
"bitflags",
"libc",
@ -154,13 +154,14 @@ checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
[[package]]
name = "libgit2-sys"
version = "0.12.21+1.1.0"
version = "0.14.2+1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86271bacd72b2b9e854c3dcfb82efd538f15f870e4c11af66900effb462f6825"
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
]
@ -224,9 +225,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "openssl-src"
version = "111.15.0+1.1.1k"
version = "111.26.0+1.1.1u"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a5f6ae2ac04393b217ea9f700cd04fa9bf3d93fae2872069f3d15d908af70a"
checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37"
dependencies = [
"cc",
]
@ -280,9 +281,9 @@ checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
[[package]]
name = "term_grid"
version = "0.2.0"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7c9eb7705cb3f0fd71d3955b23db6d372142ac139e8c473952c93bf3c3dc4b7"
checksum = "230d3e804faaed5a39b08319efb797783df2fd9671b39b7596490cb486d702cf"
dependencies = [
"unicode-width",
]

View file

@ -1,20 +1,20 @@
[package]
name = "exa"
name = "eza"
description = "A modern replacement for ls"
authors = ["Benjamin Sago <ogham@bsago.me>"]
authors = ["Benjamin Sago <ogham@bsago.me>", "Christina Sørensen <christina@cafkafk.com>"]
categories = ["command-line-utilities"]
edition = "2021"
rust-version = "1.63.0"
exclude = ["/devtools/*", "/Justfile", "/Vagrantfile", "/screenshots.png"]
readme = "README.md"
homepage = "https://the.exa.website/"
homepage = "https://github.com/cafkafk/eza"
license = "MIT"
repository = "https://github.com/ogham/exa"
repository = "https://github.com/cafkafk/ez"
version = "0.10.1"
[[bin]]
name = "exa"
name = "eza"
[dependencies]
@ -28,7 +28,7 @@ natord = "1.0"
num_cpus = "1.10"
number_prefix = "0.4"
scoped_threadpool = "0.1"
term_grid = "0.2.0"
term_grid = "0.1"
terminal_size = "0.1.16"
timeago = { version = "0.3.1", default-features = false }
unicode-width = "0.1"
@ -43,7 +43,7 @@ default-features = false
features = ["format"]
[dependencies.git2]
version = "0.13"
version = "0.16"
optional = true
default-features = false
@ -64,21 +64,23 @@ debug = false
# use LTO for smaller binaries (that take longer to build)
[profile.release]
lto = true
strip = true
opt-level = "s"
[package.metadata.deb]
license-file = [ "LICENCE", "4" ]
depends = "$auto"
extended-description = """
exa is a replacement for ls written in Rust.
eza is a replacement for ls written in Rust.
"""
section = "utils"
priority = "optional"
assets = [
[ "target/release/exa", "/usr/bin/exa", "0755" ],
[ "target/release/../man/exa.1", "/usr/share/man/man1/exa.1", "0644" ],
[ "target/release/../man/exa_colors.5", "/usr/share/man/man5/exa_colors.5", "0644" ],
[ "completions/bash/exa", "/usr/share/bash-completion/completions/exa", "0644" ],
[ "completions/zsh/_exa", "/usr/share/zsh/site-functions/_exa", "0644" ],
[ "completions/fish/exa.fish", "/usr/share/fish/vendor_completions.d/exa.fish", "0644" ],
[ "target/release/eza", "/usr/bin/eza", "0755" ],
[ "target/release/../man/eza.1", "/usr/share/man/man1/eza.1", "0644" ],
[ "target/release/../man/eza_colors.5", "/usr/share/man/man5/eza_colors.5", "0644" ],
[ "completions/bash/eza", "/usr/share/bash-completion/completions/eza", "0644" ],
[ "completions/zsh/_eza", "/usr/share/zsh/site-functions/_eza", "0644" ],
[ "completions/fish/eza.fish", "/usr/share/fish/vendor_completions.d/eza.fish", "0644" ],
]

View file

@ -98,13 +98,13 @@ all-release: build-release test-release
# build the man pages
@man:
mkdir -p "${CARGO_TARGET_DIR:-target}/man"
pandoc --standalone -f markdown -t man man/exa.1.md > "${CARGO_TARGET_DIR:-target}/man/exa.1"
pandoc --standalone -f markdown -t man man/exa_colors.5.md > "${CARGO_TARGET_DIR:-target}/man/exa_colors.5"
pandoc --standalone -f markdown -t man man/eza.1.md > "${CARGO_TARGET_DIR:-target}/man/eza.1"
pandoc --standalone -f markdown -t man man/eza_colors.5.md > "${CARGO_TARGET_DIR:-target}/man/eza_colors.5"
# build and preview the main man page (exa.1)
# build and preview the main man page (eza.1)
@man-1-preview: man
man "${CARGO_TARGET_DIR:-target}/man/exa.1"
man "${CARGO_TARGET_DIR:-target}/man/eza.1"
# build and preview the colour configuration man page (exa_colors.5)
# build and preview the colour configuration man page (eza_colors.5)
@man-5-preview: man
man "${CARGO_TARGET_DIR:-target}/man/exa_colors.5"
man "${CARGO_TARGET_DIR:-target}/man/eza_colors.5"

175
README.md
View file

@ -1,8 +1,8 @@
<div align="center">
# exa
# eza
[exa](https://the.exa.website/) is a modern replacement for _ls_.
eza is a modern replacement for _ls_.
**README Sections:** [Options](#options) — [Installation](#installation) — [Development](#development)
@ -14,22 +14,55 @@
---
**exa** is a modern replacement for the venerable file-listing command-line program `ls` that ships with Unix and Linux operating systems, giving it more features and better defaults.
**eza** is a modern replacement for the venerable file-listing command-line program `ls` that ships with Unix and Linux operating systems, giving it more features and better defaults.
It uses colours to distinguish file types and metadata.
It knows about symlinks, extended attributes, and Git.
And its **small**, **fast**, and just **one single binary**.
By deliberately making some decisions differently, exa attempts to be a more featureful, more user-friendly version of `ls`.
By deliberately making some decisions differently, eza attempts to be a more featureful, more user-friendly version of `ls`.
For more information, see [exas website](https://the.exa.website/).
---
<a id="try-it">
<h1>Try it!</h1>
</a>
### Nix ❄️
If you already have Nix setup with flake support, you can try out eza with the `nix run` command:
nix run github:cafkafk/eza
Nix will build eza and run it.
If you want to pass arguments this way, use e.g. `nix run github:cafkafk/eza -- -ol`.
<a id="installation">
<h1>Installation</h1>
</a>
eza is available for macOS and Linux.
### Cargo
If you already have a Rust environment set up, you can use the `cargo install` command:
cargo install eza
Cargo will build the `eza` binary and place it in `$HOME/.cargo`.
To build without Git support, run `cargo install --no-default-features eza` is also available, if the requisite dependencies are not installed.
---
<a id="options">
<h1>Command-line options</h1>
</a>
exas options are almost, but not quite, entirely unlike `ls`s.
ezas options are almost, but not quite, entirely unlike `ls`s.
### Display options
@ -44,6 +77,7 @@ exas options are almost, but not quite, entirely unlike `ls`s.
- **--colo[u]r-scale**: highlight levels of file sizes distinctly
- **--icons**: display icons
- **--no-icons**: don't display icons (always overrides --icons)
- **--hyperlink**: display entries as hyperlinks
### Filtering options
@ -74,12 +108,13 @@ 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 files security context
- **-@**, **--extended**: list each files extended attributes and sizes
- **--changed**: use the changed timestamp field
- **--git**: list each files Git status, if tracked or ignored
- **--time-style**: how to format timestamps
- **--no-permissions**: suppress the permissions field
- **--octal-permissions**: list each file's permission in octal format
- **-o**, **--octal-permissions**: list each file's permission in octal format
- **--no-filesize**: suppress the filesize field
- **--no-user**: suppress the user field
- **--no-time**: suppress the time field
@ -92,105 +127,6 @@ Some of the options accept parameters:
- Valid time styles are **default**, **iso**, **long-iso**, **full-iso**, and **relative**.
---
<a id="installation">
<h1>Installation</h1>
</a>
exa is available for macOS and Linux.
More information on how to install exa is available on [the Installation page](https://the.exa.website/install).
### Alpine Linux
On Alpine Linux, [enable community repository](https://wiki.alpinelinux.org/wiki/Enable_Community_Repository) and install the [`exa`](https://pkgs.alpinelinux.org/package/edge/community/x86_64/exa) package.
apk add exa
### Arch Linux
On Arch, install the [`exa`](https://www.archlinux.org/packages/community/x86_64/exa/) package.
pacman -S exa
### Android / Termux
On Android / Termux, install the [`exa`](https://github.com/termux/termux-packages/tree/master/packages/exa) package.
pkg install exa
### Debian
On Debian, install the [`exa`](https://packages.debian.org/stable/exa) package.
apt install exa
### Fedora
On Fedora, install the [`exa`](https://src.fedoraproject.org/modules/exa) package.
dnf install exa
### Gentoo
On Gentoo, install the [`sys-apps/exa`](https://packages.gentoo.org/packages/sys-apps/exa) package.
emerge sys-apps/exa
### Homebrew
If youre using [Homebrew](https://brew.sh/) on macOS, install the [`exa`](http://formulae.brew.sh/formula/exa) formula.
brew install exa
### MacPorts
If you're using [MacPorts](https://www.macports.org/) on macOS, install the [`exa`](https://ports.macports.org/port/exa/summary) port.
port install exa
### Nix
On nixOS, install the [`exa`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/exa/default.nix) package.
nix-env -i exa
### openSUSE
On openSUSE, install the [`exa`](https://software.opensuse.org/package/exa) package.
zypper install exa
### Ubuntu
On Ubuntu 20.10 (Groovy Gorilla) and later, install the [`exa`](https://packages.ubuntu.com/jammy/exa) package.
sudo apt install exa
### Void Linux
On Void Linux, install the [`exa`](https://github.com/void-linux/void-packages/blob/master/srcpkgs/exa/template) package.
xbps-install -S exa
### Manual installation from GitHub
Compiled binary versions of exa are uploaded to GitHub when a release is made.
You can install exa manually by [downloading a release](https://github.com/ogham/exa/releases), extracting it, and copying the binary to a directory in your `$PATH`, such as `/usr/local/bin`.
For more information, see the [Manual Installation page](https://the.exa.website/install/linux#manual).
### Cargo
If you already have a Rust environment set up, you can use the `cargo install` command:
cargo install exa
Cargo will build the `exa` binary and place it in `$HOME/.cargo`.
To build without Git support, run `cargo install --no-default-features exa` is also available, if the requisite dependencies are not installed.
---
<a id="development">
@ -205,11 +141,11 @@ To build without Git support, run `cargo install --no-default-features exa` is a
</a>
</h1></a>
exa is written in [Rust](https://www.rust-lang.org/).
eza is written in [Rust](https://www.rust-lang.org/).
You will need rustc version 1.56.1 or higher.
The recommended way to install Rust for development is from the [official download page](https://www.rust-lang.org/tools/install), using rustup.
Once Rust is installed, you can compile exa with Cargo:
Once Rust is installed, you can compile eza with Cargo:
cargo build
cargo test
@ -226,7 +162,7 @@ The `just man` command will compile the Markdown into manual pages, which it wil
To use them, copy them into a directory that `man` will read.
`/usr/local/share/man` is usually a good choice.
- exa depends on [libgit2](https://github.com/rust-lang/git2-rs) for certain features.
- eza depends on [libgit2](https://github.com/rust-lang/git2-rs) for certain features.
If youre unable to compile libgit2, you can opt out of Git support by running `cargo build --no-default-features`.
- If you intend to compile for musl, you will need to use the flag `vendored-openssl` if you want to get the Git feature working.
@ -234,16 +170,28 @@ The full command is `cargo build --release --target=x86_64-unknown-linux-musl --
For more information, see the [Building from Source page](https://the.exa.website/install/source).
### Developing on Nix (experimental) ❄️
If you have a working Nix installation with flake support, you can use nix to manage your dev environment.
nix develop
The Nix Flake has a few features:
- Run `nix flake check` to run `treefmt` on the repo.
- Run `nix build` and manually test `./results/bin/eza -- <arguments>` for easy debugging.
- Run `nix build .#test` to run `cargo test` via the flake.
- Run `nix build .#clippy` to lint with clippy (still work in progress).
### Testing with Vagrant
exa uses [Vagrant][] to configure virtual machines for testing.
eza uses [Vagrant][] to configure virtual machines for testing.
Programs such as exa that are basically interfaces to the system are [notoriously difficult to test][testing].
Programs such as eza that are basically interfaces to the system are [notoriously difficult to test][testing].
Although the internal components have unit tests, its impossible to do a complete end-to-end test without mandating the current users name, the time zone, the locale, and directory structure to test.
(And yes, these tests are worth doing. I have missed an edge case on many an occasion.)
The initial attempt to solve the problem was just to create a directory of “awkward” test cases, run exa on it, and make sure it produced the correct output.
The initial attempt to solve the problem was just to create a directory of “awkward” test cases, run eza on it, and make sure it produced the correct output.
But even this output would change if, say, the users locale formats dates in a different way.
These can be mocked inside the code, but at the cost of making that code more complicated to read and understand.
@ -258,7 +206,7 @@ First, initialise the VM:
host$ vagrant up
The first command downloads the virtual machine image, and then runs our provisioning script, which installs Rust and exas build-time dependencies, configures the environment, and generates some awkward files and folders to use as test cases.
The first command downloads the virtual machine image, and then runs our provisioning script, which installs Rust and ezas build-time dependencies, configures the environment, and generates some awkward files and folders to use as test cases.
Once this is done, you can SSH in, and build and test:
host$ vagrant ssh
@ -268,5 +216,6 @@ Once this is done, you can SSH in, and build and test:
All the tests passed!
Of course, the drawback of having a standard development environment is that you stop noticing bugs that occur outside of it.
For this reason, Vagrant isnt a *necessary* development step — its there if youd like to use it, but exa still gets used and tested on other platforms.
For this reason, Vagrant isnt a *necessary* development step — its there if youd like to use it, but eza still gets used and tested on other platforms.
It can still be built and compiled on any target triple that it supports, VM or no VM, with `cargo build` and `cargo test`.

View file

@ -1,4 +1,4 @@
_exa()
_eza()
{
cur=${COMP_WORDS[COMP_CWORD]}
prev=${COMP_WORDS[COMP_CWORD-1]}
@ -38,13 +38,13 @@ _exa()
# _parse_help doesnt pick up short options when they are on the same line than long options
--*)
# colo[u]r isnt parsed correctly so we filter these options out and add them by hand
parse_help=$( exa --help | grep -oE ' (\-\-[[:alnum:]@-]+)' | tr -d ' ' | grep -v '\-\-colo' )
parse_help=$( eza --help | grep -oE ' (\-\-[[:alnum:]@-]+)' | tr -d ' ' | grep -v '\-\-colo' )
completions=$( echo '--color --colour --color-scale --colour-scale' $parse_help )
COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) )
;;
-*)
completions=$( exa --help | grep -oE ' (\-[[:alnum:]@])' | tr -d ' ' )
completions=$( eza --help | grep -oE ' (\-[[:alnum:]@])' | tr -d ' ' )
COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) )
;;
@ -53,4 +53,4 @@ _exa()
;;
esac
} &&
complete -o filenames -o bashdefault -F _exa exa
complete -o filenames -o bashdefault -F _eza eza

View file

@ -1,92 +0,0 @@
# Meta-stuff
complete -c exa -s 'v' -l 'version' -d "Show version of exa"
complete -c exa -s '?' -l 'help' -d "Show list of command-line options"
# Display options
complete -c exa -s '1' -l 'oneline' -d "Display one entry per line"
complete -c exa -s 'l' -l 'long' -d "Display extended file metadata as a table"
complete -c exa -s 'G' -l 'grid' -d "Display entries in a grid"
complete -c exa -s 'x' -l 'across' -d "Sort the grid across, rather than downwards"
complete -c exa -s 'R' -l 'recurse' -d "Recurse into directories"
complete -c exa -s 'T' -l 'tree' -d "Recurse into directories as a tree"
complete -c exa -s 'F' -l 'classify' -d "Display type indicator by file names"
complete -c exa -l 'color' \
-l 'colour' -d "When to use terminal colours" -x -a "
always\t'Always use colour'
auto\t'Use colour if standard output is a terminal'
never\t'Never use colour'
"
complete -c exa -l 'color-scale' \
-l 'colour-scale' -d "Highlight levels of file sizes distinctly"
complete -c exa -l 'icons' -d "Display icons"
complete -c exa -l 'no-icons' -d "Don't display icons"
# Filtering and sorting options
complete -c exa -l 'group-directories-first' -d "Sort directories before other files"
complete -c exa -l 'git-ignore' -d "Ignore files mentioned in '.gitignore'"
complete -c exa -s 'a' -l 'all' -d "Show hidden and 'dot' files"
complete -c exa -s 'd' -l 'list-dirs' -d "List directories like regular files"
complete -c exa -s 'L' -l 'level' -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9"
complete -c exa -s 'r' -l 'reverse' -d "Reverse the sort order"
complete -c exa -s 's' -l 'sort' -d "Which field to sort by" -x -a "
accessed\t'Sort by file accessed time'
age\t'Sort by file modified time (newest first)'
changed\t'Sort by changed time'
created\t'Sort by file modified time'
date\t'Sort by file modified time'
ext\t'Sort by file extension'
Ext\t'Sort by file extension (uppercase first)'
extension\t'Sort by file extension'
Extension\t'Sort by file extension (uppercase first)'
filename\t'Sort by filename'
Filename\t'Sort by filename (uppercase first)'
inode\t'Sort by file inode'
modified\t'Sort by file modified time'
name\t'Sort by filename'
Name\t'Sort by filename (uppercase first)'
newest\t'Sort by file modified time (newest first)'
none\t'Do not sort files at all'
oldest\t'Sort by file modified time'
size\t'Sort by file size'
time\t'Sort by file modified time'
type\t'Sort by file type'
"
complete -c exa -s 'I' -l 'ignore-glob' -d "Ignore files that match these glob patterns" -r
complete -c exa -s 'D' -l 'only-dirs' -d "List only directories"
# Long view options
complete -c exa -s 'b' -l 'binary' -d "List file sizes with binary prefixes"
complete -c exa -s 'B' -l 'bytes' -d "List file sizes in bytes, without any prefixes"
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 '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'
changed\t'Display changed time'
accessed\t'Display accessed time'
created\t'Display created time'
"
complete -c exa -s 'm' -l 'modified' -d "Use the modified timestamp field"
complete -c exa -s 'n' -l 'numeric' -d "List numeric user and group IDs."
complete -c exa -l 'changed' -d "Use the changed timestamp field"
complete -c exa -s 'u' -l 'accessed' -d "Use the accessed timestamp field"
complete -c exa -s 'U' -l 'created' -d "Use the created timestamp field"
complete -c exa -l 'time-style' -d "How to format timestamps" -x -a "
default\t'Use the default time style'
iso\t'Display brief ISO timestamps'
long-iso\t'Display longer ISO timestaps, up to the minute'
full-iso\t'Display full ISO timestamps, up to the nanosecond'
relative\t'Display relative timestamps'
"
complete -c exa -l 'no-permissions' -d "Suppress the permissions field"
complete -c exa -l 'octal-permissions' -d "List each file's permission in octal format"
complete -c exa -l 'no-filesize' -d "Suppress the filesize field"
complete -c exa -l 'no-user' -d "Suppress the user field"
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"

96
completions/fish/eza.fish Executable file
View file

@ -0,0 +1,96 @@
# Meta-stuff
complete -c eza -s 'v' -l 'version' -d "Show version of eza"
complete -c eza -s '?' -l 'help' -d "Show list of command-line options"
# Display options
complete -c eza -s '1' -l 'oneline' -d "Display one entry per line"
complete -c eza -s 'l' -l 'long' -d "Display extended file metadata as a table"
complete -c eza -s 'G' -l 'grid' -d "Display entries in a grid"
complete -c eza -s 'x' -l 'across' -d "Sort the grid across, rather than downwards"
complete -c eza -s 'R' -l 'recurse' -d "Recurse into directories"
complete -c eza -s 'T' -l 'tree' -d "Recurse into directories as a tree"
complete -c eza -s 'F' -l 'classify' -d "Display type indicator by file names"
complete -c eza -l 'color' \
-l 'colour' -d "When to use terminal colours" -x -a "
always\t'Always use colour'
auto\t'Use colour if standard output is a terminal'
never\t'Never use colour'
"
complete -c eza -l 'color-scale' \
-l 'colour-scale' -d "Highlight levels of file sizes distinctly"
complete -c eza -l 'icons' -d "Display icons"
complete -c eza -l 'no-icons' -d "Don't display icons"
complete -c eza -l 'hyperlink' -d "Display entries as hyperlinks"
# Filtering and sorting options
complete -c eza -l 'group-directories-first' -d "Sort directories before other files"
complete -c eza -l 'git-ignore' -d "Ignore files mentioned in '.gitignore'"
complete -c eza -s 'a' -l 'all' -d "Show hidden and 'dot' files"
complete -c eza -s 'd' -l 'list-dirs' -d "List directories like regular files"
complete -c eza -s 'L' -l 'level' -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9"
complete -c eza -s 'r' -l 'reverse' -d "Reverse the sort order"
complete -c eza -s 's' -l 'sort' -d "Which field to sort by" -x -a "
accessed\t'Sort by file accessed time'
age\t'Sort by file modified time (newest first)'
changed\t'Sort by changed time'
created\t'Sort by file modified time'
date\t'Sort by file modified time'
ext\t'Sort by file extension'
Ext\t'Sort by file extension (uppercase first)'
extension\t'Sort by file extension'
Extension\t'Sort by file extension (uppercase first)'
filename\t'Sort by filename'
Filename\t'Sort by filename (uppercase first)'
inode\t'Sort by file inode'
modified\t'Sort by file modified time'
name\t'Sort by filename'
Name\t'Sort by filename (uppercase first)'
newest\t'Sort by file modified time (newest first)'
none\t'Do not sort files at all'
oldest\t'Sort by file modified time'
size\t'Sort by file size'
time\t'Sort by file modified time'
type\t'Sort by file type'
"
complete -c eza -s 'I' -l 'ignore-glob' -d "Ignore files that match these glob patterns" -r
complete -c eza -s 'D' -l 'only-dirs' -d "List only directories"
# Long view options
complete -c eza -s 'b' -l 'binary' -d "List file sizes with binary prefixes"
complete -c eza -s 'B' -l 'bytes' -d "List file sizes in bytes, without any prefixes"
complete -c eza -s 'g' -l 'group' -d "List each file's group"
complete -c eza -s 'h' -l 'header' -d "Add a header row to each column"
complete -c eza -s 'H' -l 'links' -d "List each file's number of hard links"
complete -c eza -s 'i' -l 'inode' -d "List each file's inode number"
complete -c eza -s 'S' -l 'blocks' -d "List each file's number of filesystem blocks"
complete -c eza -s 't' -l 'time' -d "Which timestamp field to list" -x -a "
modified\t'Display modified time'
changed\t'Display changed time'
accessed\t'Display accessed time'
created\t'Display created time'
"
complete -c eza -s 'm' -l 'modified' -d "Use the modified timestamp field"
complete -c eza -s 'n' -l 'numeric' -d "List numeric user and group IDs."
complete -c eza -l 'changed' -d "Use the changed timestamp field"
complete -c eza -s 'u' -l 'accessed' -d "Use the accessed timestamp field"
complete -c eza -s 'U' -l 'created' -d "Use the created timestamp field"
complete -c eza -l 'time-style' -d "How to format timestamps" -x -a "
default\t'Use the default time style'
iso\t'Display brief ISO timestamps'
long-iso\t'Display longer ISO timestaps, up to the minute'
full-iso\t'Display full ISO timestamps, up to the nanosecond'
relative\t'Display relative timestamps'
"
complete -c eza -l 'no-permissions' -d "Suppress the permissions field"
complete -c eza -s 'o' -l 'octal-permissions' -d "List each file's permission in octal format"
complete -c eza -l 'no-filesize' -d "Suppress the filesize field"
complete -c eza -l 'no-user' -d "Suppress the user field"
complete -c eza -l 'no-time' -d "Suppress the time field"
# Optional extras
complete -c eza -l 'git' -d "List each file's Git status, if tracked"
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 'Z' -l 'context' -d "List each file's security context"

View file

@ -1,16 +1,16 @@
#compdef exa
#compdef eza
# Save this file as _exa in /usr/local/share/zsh/site-functions or in any
# Save this file as _eza in /usr/local/share/zsh/site-functions or in any
# other folder in $fpath. E.g. save it in a folder called ~/.zfunc and add a
# line containing `fpath=(~/.zfunc $fpath)` somewhere before `compinit` in your
# ~/.zshrc.
__exa() {
__eza() {
# Give completions using the `_arguments` utility function with
# `-s` for option stacking like `exa -ab` for `exa -a -b` and
# `-S` for delimiting options with `--` like in `exa -- -a`.
# `-s` for option stacking like `eza -ab` for `eza -a -b` and
# `-S` for delimiting options with `--` like in `eza -- -a`.
_arguments -s -S \
"(- *)"{-v,--version}"[Show version of exa]" \
"(- *)"{-v,--version}"[Show version of eza]" \
"(- *)"{-'\?',--help}"[Show list of command-line options]" \
{-1,--oneline}"[Display one entry per line]" \
{-l,--long}"[Display extended file metadata as a table]" \
@ -23,6 +23,7 @@ __exa() {
--colo{,u}r-scale"[Highlight levels of file sizes distinctly]" \
--icons"[Display icons]" \
--no-icons"[Hide icons]" \
--hyperlink"[Display entries as hyperlinks]" \
--group-directories-first"[Sort directories before other files]" \
--git-ignore"[Ignore files mentioned in '.gitignore']" \
{-a,--all}"[Show hidden and 'dot' files]" \
@ -45,15 +46,18 @@ __exa() {
{-t,--time}="[Which time field to show]:(time field):(accessed changed created modified)" \
--time-style="[How to format timestamps]:(time style):(default iso long-iso full-iso relative)" \
--no-permissions"[Suppress the permissions field]" \
--octal-permissions"[List each file's permission in octal format]" \
{-o, --octal-permissions}"[List each file's permission in octal format]" \
--no-filesize"[Suppress the filesize field]" \
--no-user"[Suppress the user field]" \
--no-time"[Suppress the time field]" \
{-u,--accessed}"[Use the accessed timestamp field]" \
{-U,--created}"[Use the created timestamp field]" \
--git"[List each file's Git status, if tracked]" \
--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]" \
{-Z,--context}"[List each file's security context]" \
'*:filename:_files'
}
__exa
__eza

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# This script creates a bunch of awkward test case files. It gets
# automatically run as part of Vagrant provisioning.
trap 'exit' ERR

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# This file contains the text fixtures — the known, constant data — that are
# used when setting up the environment that exas tests get run in.

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
if [[ -f ~/target/debug/exa ]]; then
~/target/debug/exa "$@"
else

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
if [[ -f ~/target/release/exa ]]; then
~/target/release/exa "$@"
else

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
if [[ ! -d "/vagrant" ]]; then
echo "This script should be run in the Vagrant environment"

197
flake.lock Normal file
View file

@ -0,0 +1,197 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1690373729,
"narHash": "sha256-e136hTT7LqQ2QjOTZQMW+jnsevWwBpMj78u6FRUsH9I=",
"owner": "nix-community",
"repo": "naersk",
"rev": "d9a33d69a9c421d64c8d925428864e93be895dcc",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1688231357,
"narHash": "sha256-ZOn16X5jZ6X5ror58gOJAxPfFLAQhZJ6nOUeS4tfFwo=",
"path": "/nix/store/aw6kmwd8a02n2c1wysrfk2q31brlmqdz-source",
"rev": "645ff62e09d294a30de823cb568e9c6d68e92606",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1690593349,
"narHash": "sha256-i6jdORO+YiP19pFNeR7oYIIwmzQvdxwNO+BmtATcYpA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "11cf5e1c74fe6892e860afeeaf3bfb84fdb7b1c3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1680945546,
"narHash": "sha256-8FuaH5t/aVi/pR1XxnF0qi4WwMYC+YxlfdsA0V+TEuQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d9f759f2ea8d265d974a6e1259bd510ac5844c5d",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay",
"treefmt-nix": "treefmt-nix"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1690596958,
"narHash": "sha256-SWqxUiEP9O2gvlWtR4Ku6rIMGM7PuNZreAPrU2yAjsk=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "5c06b0ed7bfb00f3a925af6c4acd1636596381c1",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1689620039,
"narHash": "sha256-BtNwghr05z7k5YMdq+6nbue+nEalvDepuA7qdQMAKoQ=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "719c2977f958c41fa60a928e2fbc50af14844114",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

104
flake.nix Normal file
View file

@ -0,0 +1,104 @@
{
inputs = {
flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
treefmt-nix.url = "github:numtide/treefmt-nix";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = {
self
, flake-utils
, naersk
, nixpkgs
, treefmt-nix
, rust-overlay
}:
flake-utils.lib.eachDefaultSystem (
system: let
overlays = [(import rust-overlay)];
pkgs = (import nixpkgs) {
inherit system overlays;
};
toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
naersk' = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
treefmtEval = treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
in {
# For `nix fmt`
formatter = treefmtEval.config.build.wrapper;
# packages.default = naersk'.buildPackage {
# src = ./.;
# };
packages = {
# For `nix build` & `nix run`:
default = naersk'.buildPackage {
src = ./.;
};
# Run `nix build .#check` to check code
check = naersk'.buildPackage {
src = ./.;
mode = "check";
};
# Run `nix build .#test` to run tests
test = naersk'.buildPackage {
src = ./.;
mode = "test";
};
# Run `nix build .#clippy` to lint code
clippy = naersk'.buildPackage {
src = ./.;
mode = "clippy";
};
};
# For `nix develop`:
devShells.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [toolchain];
};
# for `nix flake check`
checks = {
formatting = treefmtEval.config.build.check self;
};
}
);
}

View file

@ -1,6 +1,6 @@
% exa(1) v0.9.0
% eza(1) v0.9.0
<!-- This is the exa(1) man page, written in Markdown. -->
<!-- This is the eza(1) man page, written in Markdown. -->
<!-- To generate the roff version, run `just man`, -->
<!-- and the man page will appear in the target directory. -->
@ -8,15 +8,15 @@
NAME
====
exa — a modern replacement for ls
eza — a modern replacement for ls
SYNOPSIS
========
`exa [options] [files...]`
`eza [options] [files...]`
**exa** is a modern replacement for `ls`.
**eza** is a modern replacement for `ls`.
It uses colours for information by default, helping you distinguish between many types of files, such as whether you are the owner, or in the owning group.
It also has extra features not present in the original `ls`, such as viewing the Git status for a directory, or recursing into directories with a tree view.
@ -25,16 +25,16 @@ It also has extra features not present in the original `ls`, such as viewing the
EXAMPLES
========
`exa`
`eza`
: Lists the contents of the current directory in a grid.
`exa --oneline --reverse --sort=size`
`eza --oneline --reverse --sort=size`
: Displays a list of files with the largest at the top.
`exa --long --header --inode --git`
`eza --long --header --inode --git`
: Displays a table of files with a header, showing each files metadata, inode, and Git status.
`exa --long --tree --level=3`
`eza --long --tree --level=3`
: Displays a tree of files, three levels deep, as well as each files metadata.
@ -75,6 +75,9 @@ Valid settings are `always`, `automatic`, and `never`.
`--no-icons`
: Don't display icons. (Always overrides --icons)
`--hyperlink`
: Display entries as hyperlinks
FILTERING AND SORTING OPTIONS
=============================
@ -104,7 +107,7 @@ Sort fields starting with a capital letter will sort uppercase before lowercase:
`-I`, `--ignore-glob=GLOBS`
: Glob patterns, pipe-separated, of files to ignore.
`--git-ignore` [if exa was built with git support]
`--git-ignore` [if eza was built with git support]
: Do not list files that are ignored by Git.
`--group-directories-first`
@ -168,6 +171,9 @@ These options are available when running with `--long` (`-l`):
`--no-permissions`
: Suppress the permissions field.
`-o`, `--octal-permissions`
: List each file's permissions in octal format.
`--no-filesize`
: Suppress the file size field.
@ -180,7 +186,10 @@ These options are available when running with `--long` (`-l`):
`-@`, `--extended`
: List each files extended attributes and sizes.
`--git` [if exa was built with git support]
`-Z`, `--context`
: List each file's security context.
`--git` [if eza was built with git support]
: List each files Git status, if tracked.
This adds a two-character column indicating the staged and unstaged statuses respectively. The status character can be `-` for not modified, `M` for a modified file, `N` for a new file, `D` for deleted, `R` for renamed, `T` for type-change, `I` for ignored, and `U` for conflicted.
@ -191,29 +200,29 @@ Directories will be shown to have the status of their contents, which is how
ENVIRONMENT VARIABLES
=====================
exa responds to the following environment variables:
eza responds to the following environment variables:
## `COLUMNS`
Overrides the width of the terminal, in characters.
For example, `COLUMNS=80 exa` will show a grid view with a maximum width of 80 characters.
For example, `COLUMNS=80 eza` will show a grid view with a maximum width of 80 characters.
This option wont do anything when exas output doesnt wrap, such as when using the `--long` view.
This option wont do anything when ezas output doesnt wrap, such as when using the `--long` view.
## `EXA_STRICT`
Enables _strict mode_, which will make exa error when two command-line options are incompatible.
Enables _strict mode_, which will make eza error when two command-line options are incompatible.
Usually, options can override each other going right-to-left on the command line, so that exa can be given aliases: creating an alias `exa=exa --sort=ext` then running `exa --sort=size` with that alias will run `exa --sort=ext --sort=size`, and the sorting specified by the user will override the sorting specified by the alias.
Usually, options can override each other going right-to-left on the command line, so that eza can be given aliases: creating an alias `eza=eza --sort=ext` then running `eza --sort=size` with that alias will run `eza --sort=ext --sort=size`, and the sorting specified by the user will override the sorting specified by the alias.
In strict mode, the two options will not co-operate, and exa will error.
In strict mode, the two options will not co-operate, and eza will error.
This option is intended for use with automated scripts and other situations where you want to be certain youre typing in the right command.
## `EXA_GRID_ROWS`
Limits the grid-details view (`exa --grid --long`) so its only activated when at least the given number of rows of output would be generated.
Limits the grid-details view (`eza --grid --long`) so its only activated when at least the given number of rows of output would be generated.
With widescreen displays, its possible for the grid to look very wide and sparse, on just one or two lines with none of the columns lining up.
By specifying a minimum number of rows, you can only use the view if its going to be worth using.
@ -222,7 +231,7 @@ By specifying a minimum number of rows, you can only use the view if its goin
Specifies the number of spaces to print between an icon (see the `--icons` option) and its file name.
Different terminals display icons differently, as they usually take up more than one character width on screen, so theres no “standard” number of spaces that exa can use to separate an icon from text. One space may place the icon too close to the text, and two spaces may place it too far away. So the choice is left up to the user to configure depending on their terminal emulator.
Different terminals display icons differently, as they usually take up more than one character width on screen, so theres no “standard” number of spaces that eza can use to separate an icon from text. One space may place the icon too close to the text, and two spaces may place it too far away. So the choice is left up to the user to configure depending on their terminal emulator.
## `NO_COLOR`
@ -234,7 +243,7 @@ See `https://no-color.org/` for details.
Specifies the colour scheme used to highlight files based on their name and kind, as well as highlighting metadata and parts of the UI.
For more information on the format of these environment variables, see the `exa_colors(5)` manual page.
For more information on the format of these environment variables, see the `eza_colors(5)` manual page.
EXIT STATUSES
@ -253,14 +262,14 @@ EXIT STATUSES
AUTHOR
======
exa is maintained by Benjamin ogham Sago and many other contributors.
eza is maintained by Christina Sørensen and many other contributors.
**Website:** `https://the.exa.website/` \
**Source code:** `https://github.com/ogham/exa` \
**Contributors:** `https://github.com/ogham/exa/graphs/contributors`
**Source code:** `https://github.com/cafkafk/eza` \
**Contributors:** `https://github.com/cafkafk/eza/graphs/contributors`
Our infinite thanks to Benjamin ogham Sago and all the other contributors of exa, from which eza was forked.
SEE ALSO
========
- `exa_colors(5)`
- `eza_colors(5)`

View file

@ -1,6 +1,6 @@
% exa_colors(5) v0.9.0
% eza_colors(5) v0.9.0
<!-- This is the exa_colors(5) man page, written in Markdown. -->
<!-- This is the eza_colors(5) man page, written in Markdown. -->
<!-- To generate the roff version, run `just man`, -->
<!-- and the man page will appear in the target directory. -->
@ -8,13 +8,13 @@
NAME
====
exa_colors — customising the file and UI colours of exa
eza_colors — customising the file and UI colours of eza
SYNOPSIS
========
The `EXA_COLORS` environment variable can be used to customise the colours that `exa` uses to highlight file names, file metadata, and parts of the UI.
The `EXA_COLORS` environment variable can be used to customise the colours that `eza` uses to highlight file names, file metadata, and parts of the UI.
You can use the `dircolors` program to generate a script that sets the variable from an input file, or if you dont mind editing long strings of text, you can just type it out directly. These variables have the following structure:
@ -223,9 +223,9 @@ Values in `EXA_COLORS` override those given in `LS_COLORS`, so you dont need
LIST OF STYLES
==============
Unlike some versions of `ls`, the given ANSI values must be valid colour codes: exa wont just print out whichever characters are given.
Unlike some versions of `ls`, the given ANSI values must be valid colour codes: eza wont just print out whichever characters are given.
The codes accepted by exa are:
The codes accepted by eza are:
`1`
: for bold
@ -259,8 +259,8 @@ The codes accepted by exa are:
Many terminals will treat bolded text as a different colour, or at least provide the option to.
exa provides its own built-in set of file extension mappings that cover a large range of common file extensions, including documents, archives, media, and temporary files.
Any mappings in the environment variables will override this default set: running exa with `LS_COLORS="*.zip=32"` will turn zip files green but leave the colours of other compressed files alone.
eza provides its own built-in set of file extension mappings that cover a large range of common file extensions, including documents, archives, media, and temporary files.
Any mappings in the environment variables will override this default set: running eza with `LS_COLORS="*.zip=32"` will turn zip files green but leave the colours of other compressed files alone.
You can also disable this built-in set entirely by including a `reset` entry at the beginning of `EXA_COLORS`.
So setting `EXA_COLORS="reset:*.txt=31"` will highlight only text files; setting `EXA_COLORS="reset"` will highlight nothing.
@ -269,14 +269,15 @@ So setting `EXA_COLORS="reset:*.txt=31"` will highlight only text files; setting
AUTHOR
======
exa is maintained by Benjamin ogham Sago and many other contributors.
eza is maintained by Christina Sørensen and many other contributors.
**Website:** `https://the.exa.website/` \
**Source code:** `https://github.com/ogham/exa` \
**Contributors:** `https://github.com/ogham/exa/graphs/contributors`
**Source code:** `https://github.com/cafkafk/eza` \
**Contributors:** `https://github.com/cafkafk/eza/graphs/contributors`
Our infinite thanks to Benjamin ogham Sago and all the other contributors of exa, from which eza was forked.
SEE ALSO
========
- `exa(1)`
- `eza(1)`

View file

@ -1,2 +1,4 @@
[toolchain]
channel = "1.63.0"
channel = "nightly"
components = [ "rustfmt", "rustc", "rust-src", "rust-analyzer", "cargo" ]
profile = "minimal"

View file

@ -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()
}
}

View file

@ -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")
}
}
}

View file

@ -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 bufsize = lister.listxattr_first(&c_path);
match bufsize.cmp(&0) {
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Equal => return Ok(Vec::new()),
Ordering::Greater => {},
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 => (),
}
let mut buf = vec![0_u8; bufsize as usize];
let err = lister.listxattr_second(&c_path, &mut buf, bufsize);
Ok(vec![Attribute {
name: String::from(SELINUX_XATTR_NAME),
value: lister.translate_attribute_data(&buf_value),
}])
}
match err.cmp(&0) {
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);
let bufsize = match bufsize.cmp(&0) {
Ordering::Less => return Err(io::Error::last_os_error()),
// 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];
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]);
for attr_name in buf.split(|c| c == &0) {
if attr_name.is_empty() {
continue;
}
if size > 0 {
names.push(Attribute {
name: lister.translate_attribute_name(&buf[start..end]),
size: size as usize,
});
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().cast::<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().cast::<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,
)
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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> {
/// directorys 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 files 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()
@ -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) => {
@ -501,6 +525,16 @@ impl<'dir> File<'dir> {
pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
choices.contains(&&self.name[..])
}
/// This files 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 }
}
}

View file

@ -50,7 +50,7 @@ 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),
}

View file

@ -2,15 +2,16 @@ use crate::options::{flags, OptionsError, NumberSource};
use crate::options::parser::MatchedFlags;
use crate::options::vars::{self, Vars};
use crate::output::file_name::{Options, Classify, ShowIcons};
use crate::output::file_name::{Options, Classify, ShowIcons, EmbedHyperlinks};
impl Options {
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
let classify = Classify::deduce(matches)?;
let show_icons = ShowIcons::deduce(matches, vars)?;
let embed_hyperlinks = EmbedHyperlinks::deduce(matches)?;
Ok(Self { classify, show_icons })
Ok(Self { classify, show_icons, embed_hyperlinks })
}
}
@ -44,3 +45,12 @@ impl ShowIcons {
}
}
}
impl EmbedHyperlinks {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
let flagged = matches.has(&flags::HYPERLINK)?;
if flagged { Ok(Self::On) }
else { Ok(Self::Off) }
}
}

View file

@ -51,6 +51,7 @@ pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_
pub static ACCESSED: Arg = Arg { short: Some(b'u'), long: "accessed", takes_value: TakesValue::Forbidden };
pub static CREATED: Arg = Arg { short: Some(b'U'), long: "created", takes_value: TakesValue::Forbidden };
pub static TIME_STYLE: Arg = Arg { short: None, long: "time-style", takes_value: TakesValue::Necessary(Some(TIME_STYLES)) };
pub static HYPERLINK: Arg = Arg { short: None, long: "hyperlink", takes_value: TakesValue::Forbidden};
const TIMES: Values = &["modified", "changed", "accessed", "created"];
const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso", "relative"];
@ -62,9 +63,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: None, 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(&[
@ -77,8 +81,8 @@ pub static ALL_ARGS: Args = Args(&[
&IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS,
&BINARY, &BYTES, &GROUP, &NUMERIC, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED,
&BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE,
&BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK,
&NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &NO_ICONS,
&GIT, &EXTENDED, &OCTAL
&GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT, &EXTENDED, &OCTAL, &SECURITY_CONTEXT
]);

View file

@ -24,6 +24,7 @@ DISPLAY OPTIONS
--colo[u]r-scale highlight levels of file sizes distinctly
--icons display icons
--no-icons don't display icons (always overrides --icons)
--hyperlink display entries as hyperlinks
FILTERING AND SORTING OPTIONS
-a, --all show hidden and 'dot' files
@ -41,30 +42,30 @@ FILTERING AND SORTING OPTIONS
date, time, old, and new all refer to modified.
LONG VIEW OPTIONS
-b, --binary list file sizes with binary prefixes
-B, --bytes list file sizes in bytes, without any prefixes
-g, --group list each file's group
-h, --header add a header row to each column
-H, --links list each file's number of hard links
-i, --inode list each file's inode number
-m, --modified use the modified timestamp field
-n, --numeric list numeric user and group IDs
-S, --blocks show number of file system blocks
-t, --time FIELD which timestamp field to list (modified, accessed, created)
-u, --accessed use the accessed timestamp field
-U, --created use the created timestamp field
--changed use the changed timestamp field
--time-style how to format timestamps (default, iso, long-iso, full-iso, relative)
--no-permissions suppress the permissions field
--octal-permissions list each file's permission in octal format
--no-filesize suppress the filesize field
--no-user suppress the user field
--no-time suppress the time field";
-b, --binary list file sizes with binary prefixes
-B, --bytes list file sizes in bytes, without any prefixes
-g, --group list each file's group
-h, --header add a header row to each column
-H, --links list each file's number of hard links
-i, --inode list each file's inode number
-m, --modified use the modified timestamp field
-n, --numeric list numeric user and group IDs
-S, --blocks show number of file system blocks
-t, --time FIELD which timestamp field to list (modified, accessed, created)
-u, --accessed use the accessed timestamp field
-U, --created use the created timestamp field
--changed use the changed timestamp field
--time-style how to format timestamps (default, iso, long-iso, full-iso, relative)
--no-permissions suppress the permissions field
-o, --octal-permissions list each file's permission in octal format
--no-filesize suppress the filesize field
--no-user suppress the user field
--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 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
@ -110,6 +111,7 @@ impl fmt::Display for HelpString {
if xattr::ENABLED {
write!(f, "\n{}", EXTENDED_HELP)?;
write!(f, "\n{}", SECATTR_HELP)?;
}
writeln!(f)

View file

@ -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 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 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 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 })
}
}

View file

@ -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;
@ -105,6 +106,9 @@ pub struct Options {
/// Whether to show each files 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>,
@ -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 *hasnt* been specified, then
// display the @, but dont 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 }
}

View file

@ -19,6 +19,9 @@ pub struct Options {
/// Whether to prepend icon characters before file names.
pub show_icons: ShowIcons,
/// Whether to make file names hyperlinks.
pub embed_hyperlinks: EmbedHyperlinks,
}
impl Options {
@ -84,6 +87,13 @@ pub enum ShowIcons {
On(u32),
}
/// Whether to embed hyperlinks.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum EmbedHyperlinks{
Off,
On,
}
/// A **file name** holds all the information necessary to display the name
/// of the given file. This is used in all of the views.
@ -151,7 +161,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
// indicate this fact. But when showing targets, we can just
// colour the path instead (see below), and leave the broken
// links filename as the link colour.
for bit in self.coloured_file_name() {
for bit in self.escaped_file_name() {
bits.push(bit);
}
}
@ -171,6 +181,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
let target_options = Options {
classify: Classify::JustFilenames,
show_icons: ShowIcons::Off,
embed_hyperlinks: EmbedHyperlinks::Off,
};
let target_name = FileName {
@ -181,7 +192,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
options: target_options,
};
for bit in target_name.coloured_file_name() {
for bit in target_name.escaped_file_name() {
bits.push(bit);
}
@ -279,6 +290,8 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
/// Returns at least one ANSI-highlighted string representing this files
/// name using the given set of colours.
///
/// If --hyperlink flag is provided, it will escape the filename accordingly.
///
/// Ordinarily, this will be just one string: the files complete name,
/// coloured according to its file type. If the name contains control
/// characters such as newlines or escapes, though, we cant just print them
@ -286,12 +299,11 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
///
/// So in that situation, those characters will be escaped and highlighted in
/// a different colour.
fn coloured_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
fn escaped_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
let file_style = self.style();
let mut bits = Vec::new();
escape(
self.file.name.clone(),
self.escape_color_and_hyperlinks(
&mut bits,
file_style,
self.colours.control_char(),
@ -300,6 +312,52 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
bits
}
// An adapted version of escape::escape.
// afaik of all the calls to escape::escape, only for escaped_file_name, the call to escape needs to be checked for hyper links
// and if that's the case then I think it's best to not try and generalize escape::escape to this case,
// as this adaptation would incur some unneeded operations there
pub fn escape_color_and_hyperlinks(&self, bits: &mut Vec<ANSIString<'_>>, good: Style, bad: Style) {
let string = self.file.name.to_owned();
if string.chars().all(|c| c >= 0x20 as char && c != 0x7f as char) {
let painted = good.paint(string);
let adjusted_filename = if let EmbedHyperlinks::On = self.options.embed_hyperlinks {
ANSIString::from(format!("\x1B]8;;{}\x1B\x5C{}\x1B]8;;\x1B\x5C", self.file.path.display(), painted))
} else {
painted
};
bits.push(adjusted_filename);
return;
}
// again adapted from escape::escape
// still a slow route, but slightly improved to at least not reallocate buff + have a predetermined buff size
//
// also note that buff would never need more than len,
// even tho 'in total' it will be lenghier than len (as we expand with escape_default),
// because we clear it after an irregularity
let mut buff = String::with_capacity(string.len());
for c in string.chars() {
// The `escape_default` method on `char` is *almost* what we want here, but
// it still escapes non-ASCII UTF-8 characters, which are still printable.
if c >= 0x20 as char && c != 0x7f as char {
buff.push(c);
}
else {
if ! buff.is_empty() {
bits.push(good.paint(std::mem::take(&mut buff)));
}
// biased towards regular characters, so we still collect on first sight of bad char
for e in c.escape_default() {
buff.push(e);
}
bits.push(bad.paint(std::mem::take(&mut buff)));
}
}
}
/// Figures out which colour to paint the filename part of the output,
/// depending on which “type” of file it appears to be — either from the
/// class on the filesystem or from its name. (Or the broken link colour,
@ -330,6 +388,11 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
_ => self.colours.colour_file(self.file),
}
}
/// For grid's use, to cover the case of hyperlink escape sequences
pub fn bare_width(&self) -> usize {
self.file.name.len()
}
}

View file

@ -41,12 +41,14 @@ impl<'a> Render<'a> {
self.filter.sort_files(&mut self.files);
for file in &self.files {
let filename = self.file_style.for_file(file, self.theme).paint();
let filename = self.file_style.for_file(file, self.theme);
let contents = filename.paint();
grid.add(tg::Cell {
contents: filename.strings().to_string(),
width: *filename.width(),
alignment: tg::Alignment::Left,
contents: contents.strings().to_string(),
// with hyperlink escape sequences,
// the actual *contents.width() is larger than actually needed, so we take only the filename
width: filename.bare_width(),
});
}

View file

@ -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};
@ -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()
@ -263,7 +262,6 @@ impl<'a> Render<'a> {
let cell = grid::Cell {
contents: ANSIStrings(&column[row].contents).to_string(),
width: *column[row].width,
alignment: grid::Alignment::Left,
};
grid.add(cell);
@ -277,7 +275,6 @@ impl<'a> Render<'a> {
let cell = grid::Cell {
contents: ANSIStrings(&cell.contents).to_string(),
width: *cell.width,
alignment: grid::Alignment::Left,
};
grid.add(cell);
@ -299,11 +296,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,
}
}

View file

@ -57,6 +57,7 @@ lazy_static! {
m.insert(".gitconfig", '\u{f1d3}'); // 
m.insert(".github", '\u{f408}'); // 
m.insert(".gitignore", '\u{f1d3}'); // 
m.insert(".gitignore_global", '\u{f1d3}'); // 
m.insert(".gitmodules", '\u{f1d3}'); // 
m.insert(".rvm", '\u{e21e}'); // 
m.insert(".vimrc", '\u{e62b}'); // 
@ -88,6 +89,7 @@ lazy_static! {
m.insert("PKGBUILD", '\u{f303}'); // 
m.insert("rubydoc", '\u{e73b}'); // 
m.insert("yarn.lock", '\u{e718}'); // 
m.insert("Vagrantfile", '\u{2371}'); //⍱
m
};
@ -360,6 +362,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
"xz" => '\u{f410}', // 
"yaml" => '\u{f481}', // 
"yml" => '\u{f481}', // 
"zig" => '\u{21af}', // ↯
"zip" => '\u{f410}', // 
"zsh" => '\u{f489}', // 
"zsh-theme" => '\u{f489}', // 

View file

@ -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> {

View file

@ -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;

View file

@ -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| {

View file

@ -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;
}

View file

@ -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
@ -193,8 +212,12 @@ 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",
}
}
}
@ -324,7 +347,7 @@ impl Environment {
Some(t)
}
Err(ref e) => {
println!("Unable to determine time zone: {}", e);
eprintln!("Unable to determine time zone: {e}");
None
}
};
@ -494,9 +517,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)
@ -525,6 +558,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();

View file

@ -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(),

View file

@ -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,

View file

@ -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()

8
treefmt.nix Normal file
View file

@ -0,0 +1,8 @@
{
projectRootFile = "Cargo.toml";
programs = {
# alejandra.enable = true;
rustfmt.enable = true;
};
}

View file

@ -33,24 +33,24 @@ FILTERING AND SORTING OPTIONS
date, time, old, and new all refer to modified.
LONG VIEW OPTIONS
-b, --binary list file sizes with binary prefixes
-B, --bytes list file sizes in bytes, without any prefixes
-g, --group list each file's group
-h, --header add a header row to each column
-H, --links list each file's number of hard links
-i, --inode list each file's inode number
-m, --modified use the modified timestamp field
-n, --numeric list numeric user and group IDs
-S, --blocks show number of file system blocks
-t, --time FIELD which timestamp field to list (modified, accessed, created)
-u, --accessed use the accessed timestamp field
-U, --created use the created timestamp field
--changed use the changed timestamp field
--time-style how to format timestamps (default, iso, long-iso, full-iso, relative)
--no-permissions suppress the permissions field
--octal-permissions list each file's permission in octal format
--no-filesize suppress the filesize field
--no-user suppress the user field
--no-time suppress the time field
--git list each file's Git status, if tracked or ignored
-@, --extended list each file's extended attributes and sizes
-b, --binary list file sizes with binary prefixes
-B, --bytes list file sizes in bytes, without any prefixes
-g, --group list each file's group
-h, --header add a header row to each column
-H, --links list each file's number of hard links
-i, --inode list each file's inode number
-m, --modified use the modified timestamp field
-n, --numeric list numeric user and group IDs
-S, --blocks show number of file system blocks
-t, --time FIELD which timestamp field to list (modified, accessed, created)
-u, --accessed use the accessed timestamp field
-U, --created use the created timestamp field
--changed use the changed timestamp field
--time-style how to format timestamps (default, iso, long-iso, full-iso, relative)
--no-permissions suppress the permissions field
-o, --octal-permissions list each file's permission in octal format
--no-filesize suppress the filesize field
--no-user suppress the user field
--no-time suppress the time field
--git list each file's Git status, if tracked or ignored
-@, --extended list each file's extended attributes and sizes

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
trap 'exit' ERR
# Check for release mode