mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-11-05 20:37:52 +00:00
Merge branch 'master' into cargo-deb-spec
This commit is contained in:
commit
5683b5796e
17
.gitignore
vendored
17
.gitignore
vendored
@ -1,4 +1,21 @@
|
||||
# Rust stuff
|
||||
target
|
||||
|
||||
# Vagrant stuff
|
||||
.vagrant
|
||||
ubuntu-xenial-16.04-cloudimg-console.log
|
||||
|
||||
# Compiled artifacts
|
||||
# (see devtools/*-package-for-*.sh)
|
||||
/exa-linux-x86_64
|
||||
/exa-linux-x86_64-*.zip
|
||||
/exa-macos-x86_64
|
||||
/exa-macos-x86_64-*.zip
|
||||
/MD5SUMS
|
||||
/SHA1SUMS
|
||||
|
||||
# Snap stuff
|
||||
parts
|
||||
prime
|
||||
stage
|
||||
*.snap
|
||||
|
@ -8,4 +8,6 @@ rust:
|
||||
- stable
|
||||
script:
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
- cargo test --verbose
|
||||
- cargo build --verbose --no-default-features
|
||||
- cargo test --verbose --no-default-features
|
||||
|
580
Cargo.lock
generated
580
Cargo.lock
generated
@ -1,112 +1,141 @@
|
||||
[root]
|
||||
name = "exa"
|
||||
version = "0.7.0"
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ansi_term 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"datetime 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"git2 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num_cpus 1.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"number_prefix 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"scoped_threadpool 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"term_grid 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"users 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"zoneinfo_compiled 0.2.1 (git+https://github.com/rust-datetime/zoneinfo-compiled.git)",
|
||||
"memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.8.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "0.9.1"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "0.4.2"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.24"
|
||||
name = "cc"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "conv"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "custom_derive"
|
||||
name = "cfg-if"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "datetime"
|
||||
version = "0.4.4"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"iso8601 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pad 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gcc"
|
||||
version = "0.3.51"
|
||||
name = "env_logger"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
name = "exa"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"ansi_term 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"datetime 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"git2 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"term_grid 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"users 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"zoneinfo_compiled 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.6.6"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libgit2-sys 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libgit2-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"openssl-sys 0.9.47 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.2.11"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.1.4"
|
||||
name = "humantime"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -117,37 +146,45 @@ dependencies = [
|
||||
"nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "0.2.8"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.29"
|
||||
version = "0.2.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.6.13"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cmake 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libz-sys 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.0.16"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -155,29 +192,25 @@ name = "locale"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "magenta"
|
||||
version = "0.1.1"
|
||||
name = "log"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"magenta-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "magenta-sys"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@ -191,227 +224,332 @@ version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.1.40"
|
||||
name = "num-traits"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-bigint 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-complex 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-iter 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-rational 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.1.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.1.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.1.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-bigint 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.1.40"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.6.2"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.2.7"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "111.3.0+1.1.1c"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"openssl-src 111.3.0+1.1.1c (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pad"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.9"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.3.16"
|
||||
name = "quick-error"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"magenta 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-serialize"
|
||||
version = "0.3.24"
|
||||
name = "regex"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped_threadpool"
|
||||
version = "0.1.7"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "term_grid"
|
||||
version = "0.1.5"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term_size"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ucd-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "1.5.1"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"percent-encoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "users"
|
||||
version = "0.5.3"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-ranges"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.2"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "zoneinfo_compiled"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/rust-datetime/zoneinfo-compiled.git#f56921ea5e9f7cf065b1480ff270a1757c1f742f"
|
||||
name = "winapi"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"byteorder 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"datetime 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-build"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "wincolor"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zoneinfo_compiled"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"datetime 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[metadata]
|
||||
"checksum ansi_term 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c877397e09fec7a240af5fa74ad0124054b8066149d6544cd1ace93f8de3be68"
|
||||
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
|
||||
"checksum byteorder 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "96c8b41881888cc08af32d47ac4edd52bc7fa27fef774be47a92443756451304"
|
||||
"checksum cmake 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b8ebbb35d3dc9cd09497168f33de1acb79b265d350ab0ac34133b98f8509af1f"
|
||||
"checksum conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299"
|
||||
"checksum custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9"
|
||||
"checksum datetime 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2d425bf1f6bbd57cf833081c1e60ac294fd74e7edd66acc91c3fca2e496bcee9"
|
||||
"checksum gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)" = "120d07f202dcc3f72859422563522b66fe6463a4c513df062874daad05f85f0a"
|
||||
"checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685"
|
||||
"checksum git2 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aa01936ac96555c083c0e8553f672616274408d9d3fc5b8696603fbf63ff43ee"
|
||||
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
|
||||
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
|
||||
"checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c"
|
||||
"checksum ansi_term 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eaa72766c3585a1f812a3387a7e2c6cab780f899c2f43ff6ea06c8d071fcbb36"
|
||||
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
|
||||
"checksum autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0e49efa51329a5fd37e7c79db4621af617cd4e3e5bc224939808d076077077bf"
|
||||
"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
|
||||
"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
|
||||
"checksum cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)" = "5e5f3fee5eeb60324c2781f1e41286bdee933850fff9b3c672587fed5ec58c83"
|
||||
"checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4"
|
||||
"checksum datetime 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5c44b6c112860e38412e0c4732172d723458d40db906ee4b9ce87544f022a7b9"
|
||||
"checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a"
|
||||
"checksum git2 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "924b2e7d2986e625dcad89e8a429a7b3adee3c3d71e585f4a66c4f7e78715e31"
|
||||
"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
|
||||
"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
|
||||
"checksum iso8601 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "11dc464f8c6f17595d191447c9c6559298b2d023d6f846a4a23ac7ea3c46c477"
|
||||
"checksum lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3b37545ab726dd833ec6420aaba8231c5b320814b9029ad585555d2a03e94fbf"
|
||||
"checksum libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)" = "8a014d9226c2cc402676fbe9ea2e15dd5222cd1dd57f576b5b283178c944a264"
|
||||
"checksum libgit2-sys 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "0f1641ccb55181967a3e5ee4ae2911c0563492f016383ea67a27886181de088c"
|
||||
"checksum libz-sys 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "3fdd64ef8ee652185674455c1d450b83cbc8ad895625d543b5324d923f82e4d8"
|
||||
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||
"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
|
||||
"checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917"
|
||||
"checksum libgit2-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "941a41e23f77323b8c9d2ee118aec9ee39dfc176078c18b4757d3bad049d9ff7"
|
||||
"checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe"
|
||||
"checksum locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd"
|
||||
"checksum magenta 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf0336886480e671965f794bc9b6fce88503563013d1bfb7a502c81fe3ac527"
|
||||
"checksum magenta-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40d014c7011ac470ae28e2f76a02bfea4a8480f73e701353b49ad7a8d75f4699"
|
||||
"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376"
|
||||
"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
|
||||
"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
|
||||
"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
|
||||
"checksum natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
|
||||
"checksum nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
|
||||
"checksum num 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "a311b77ebdc5dd4cf6449d81e4135d9f0e3b153839ac90e648a8ef538f923525"
|
||||
"checksum num-bigint 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "8fd0f8dbb4c0960998958a796281d88c16fbe68d87b1baa6f31e2979e81fd0bd"
|
||||
"checksum num-complex 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "503e668405c5492d67cf662a81e05be40efe2e6bcf10f7794a07bd9865e704e6"
|
||||
"checksum num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "d1452e8b06e448a07f0e6ebb0bb1d92b8890eea63288c0b627331d53514d0fba"
|
||||
"checksum num-iter 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)" = "7485fcc84f85b4ecd0ea527b14189281cf27d60e583ae65ebc9c088b13dffe01"
|
||||
"checksum num-rational 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "288629c76fac4b33556f4b7ab57ba21ae202da65ba8b77466e6d598e31990790"
|
||||
"checksum num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "99843c856d68d8b4313b03a17e33c4bb42ae8f6610ea81b28abe076ac721b9b0"
|
||||
"checksum num_cpus 1.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aec53c34f2d0247c5ca5d32cca1478762f301740468ee9ee6dcb7a0dd7a0c584"
|
||||
"checksum number_prefix 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "59a14be9c211cb9c602bad35ac99f41e9a84b44d71b8cbd3040e3bd02a214902"
|
||||
"checksum pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d1bf3336e626b898e7263790d432a711d4277e22faea20dd9f70e0cab268fa58"
|
||||
"checksum percent-encoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de154f638187706bde41d9b4738748933d64e6b37bdbffc0b47a97d16a6ae356"
|
||||
"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903"
|
||||
"checksum rand 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "eb250fd207a4729c976794d03db689c9be1d634ab5a1c9da9492a13d8fecbcdf"
|
||||
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
||||
"checksum scoped_threadpool 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3ef399c8893e8cb7aa9696e895427fab3a6bf265977bb96e126f24ddd2cda85a"
|
||||
"checksum term_grid 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ccc202875496cf72a683a1ecd66f0742a830e73c202bdbd21867d73dfaac8343"
|
||||
"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
|
||||
"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1"
|
||||
"checksum num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a23f0ed30a54abaa0c7e83b1d2d87ada7c3c23078d1d87815af3e3b6385fbba"
|
||||
"checksum number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
|
||||
"checksum openssl-src 111.3.0+1.1.1c (registry+https://github.com/rust-lang/crates.io-index)" = "53ed5f31d294bdf5f7a4ba0a206c2754b0f60e9a63b7e3076babc5317873c797"
|
||||
"checksum openssl-sys 0.9.47 (registry+https://github.com/rust-lang/crates.io-index)" = "75bdd6dbbb4958d38e47a1d2348847ad1eb4dc205dc5d37473ae504391865acc"
|
||||
"checksum pad 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9b8de33465981073e32e1d75bb89ade49062bb853e7c97ec2c13439095563a"
|
||||
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
|
||||
"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
|
||||
"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
|
||||
"checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252"
|
||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||
"checksum regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8f0a0bcab2fd7d1d7c54fa9eae6f43eddeb9ce2e7352f8518a814a4f65d60c58"
|
||||
"checksum regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dcfd8681eebe297b81d98498869d4aae052137651ad7b96822f09ceb690d0a96"
|
||||
"checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
|
||||
"checksum smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c4488ae950c49d403731982257768f48fada354a5203fe81f9bb6f43ca9002be"
|
||||
"checksum term_grid 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "230d3e804faaed5a39b08319efb797783df2fd9671b39b7596490cb486d702cf"
|
||||
"checksum term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9e5b9a66db815dcfd2da92db471106457082577c3c278d4138ab3e3b4e189327"
|
||||
"checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f"
|
||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
|
||||
"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
|
||||
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
|
||||
"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f"
|
||||
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
|
||||
"checksum url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eeb819346883532a271eb626deb43c4a1bb4c4dd47c519bd78137c3e72a4fe27"
|
||||
"checksum users 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e7d8fb16f17ce0e6a18a25ce39f08edb5fbe9a25f3f346c9dca5e6ffc0485cdf"
|
||||
"checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b"
|
||||
"checksum zoneinfo_compiled 0.2.1 (git+https://github.com/rust-datetime/zoneinfo-compiled.git)" = "<none>"
|
||||
"checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426"
|
||||
"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
|
||||
"checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
|
||||
"checksum users 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c72f4267aea0c3ec6d07eaabea6ead7c5ddacfafc5e22bcf8d186706851fb4cf"
|
||||
"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
|
||||
"checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d"
|
||||
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||
"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770"
|
||||
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
|
||||
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba"
|
||||
"checksum zoneinfo_compiled 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6e136f8e8905dcf086773dbb987be12b149e240bf4039dce9d068774780ad52e"
|
||||
|
56
Cargo.toml
56
Cargo.toml
@ -1,7 +1,8 @@
|
||||
[package]
|
||||
name = "exa"
|
||||
version = "0.7.0"
|
||||
authors = [ "ogham@bsago.me" ]
|
||||
version = "0.9.0"
|
||||
authors = [ "Benjamin Sago <ogham@bsago.me>" ]
|
||||
build = "build.rs"
|
||||
|
||||
description = "A modern replacement for ls"
|
||||
homepage = "https://the.exa.website/"
|
||||
@ -12,34 +13,50 @@ readme = "README.md"
|
||||
categories = ["command-line-utilities"]
|
||||
keywords = ["ls", "files", "command-line"]
|
||||
license = "MIT"
|
||||
exclude = ["/devtools/*", "/Makefile", "/Vagrantfile", "/screenshots.png"]
|
||||
|
||||
|
||||
[[bin]]
|
||||
name = "exa"
|
||||
path = "src/bin/main.rs"
|
||||
doc = false
|
||||
|
||||
|
||||
[lib]
|
||||
name = "exa"
|
||||
path = "src/exa.rs"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.8.0"
|
||||
datetime = "0.4.3"
|
||||
getopts = "0.2.14"
|
||||
glob = "0.2"
|
||||
lazy_static = "0.2"
|
||||
libc = "0.2.9"
|
||||
locale = "0.2.1"
|
||||
natord = "1.0.7"
|
||||
num_cpus = "1.3.0"
|
||||
number_prefix = "0.2.3"
|
||||
scoped_threadpool = "0.1.*"
|
||||
term_grid = "0.1.2"
|
||||
unicode-width = "0.1.4"
|
||||
users = "0.5.2"
|
||||
ansi_term = "0.12.0"
|
||||
datetime = "0.4.7"
|
||||
env_logger = "0.6.1"
|
||||
glob = "0.3.0"
|
||||
lazy_static = "1.3.0"
|
||||
libc = "0.2.51"
|
||||
locale = "0.2.2"
|
||||
log = "0.4.6"
|
||||
natord = "1.0.9"
|
||||
num_cpus = "1.10.0"
|
||||
number_prefix = "0.3.0"
|
||||
scoped_threadpool = "0.1.9"
|
||||
term_grid = "0.1.7"
|
||||
term_size = "0.3.1"
|
||||
unicode-width = "0.1.5"
|
||||
users = "0.9.1"
|
||||
zoneinfo_compiled = "0.4.8"
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.9.1"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[build-dependencies]
|
||||
datetime = "0.4.7"
|
||||
|
||||
[features]
|
||||
default = [ "git" ]
|
||||
git = [ "git2" ]
|
||||
vendored-openssl = ["git2/vendored-openssl"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
@ -47,13 +64,6 @@ debug = false
|
||||
lto = true
|
||||
panic = "abort"
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.6.4"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.zoneinfo_compiled]
|
||||
git = "https://github.com/rust-datetime/zoneinfo-compiled.git"
|
||||
|
||||
[package.metadata.deb]
|
||||
license-file = [ "LICENCE" ]
|
||||
|
12
Makefile
12
Makefile
@ -8,17 +8,17 @@ endif
|
||||
endef
|
||||
|
||||
$(eval $(call compdir,BASHDIR,bash-completion,$(PREFIX)/etc/bash_completion.d))
|
||||
ZSHDIR = /usr/share/zsh/vendor-completions
|
||||
$(eval $(call compdir,ZSHDIR,zsh,/usr/share/zsh/vendor_completions.d))
|
||||
$(eval $(call compdir,FISHDIR,fish,$(PREFIX)/share/fish/vendor_completions.d))
|
||||
|
||||
FEATURES ?= default
|
||||
|
||||
CARGO_OPTS := --no-default-features --features "$(FEATURES)"
|
||||
|
||||
all: target/release/exa
|
||||
build: target/release/exa
|
||||
|
||||
target/release/exa:
|
||||
cargo build --release --no-default-features --features "$(FEATURES)"
|
||||
cargo build --release $(CARGO_OPTS)
|
||||
|
||||
install: install-exa install-man
|
||||
|
||||
@ -38,6 +38,11 @@ install-zsh-completions:
|
||||
install-fish-completions:
|
||||
install -m644 -- contrib/completions.fish "$(DESTDIR)$(FISHDIR)/exa.fish"
|
||||
|
||||
test: target/release/exa
|
||||
cargo test --release $(CARGO_OPTS)
|
||||
|
||||
check: test
|
||||
|
||||
uninstall:
|
||||
-rm -f -- "$(DESTDIR)$(PREFIX)/share/man/man1/exa.1"
|
||||
-rm -f -- "$(DESTDIR)$(PREFIX)/bin/exa"
|
||||
@ -59,6 +64,7 @@ help:
|
||||
@echo ' install - build and install exa and manpage'
|
||||
@echo ' install-exa - build and install exa'
|
||||
@echo ' install-man - install the manpage'
|
||||
@echo ' test - run `cargo test`'
|
||||
@echo ' uninstall - uninstall fish, manpage, and completions'
|
||||
@echo ' preview-man - preview the manpage without installing'
|
||||
@echo ' help - print this help'
|
||||
|
45
README.md
45
README.md
@ -2,6 +2,12 @@
|
||||
|
||||
[exa](https://the.exa.website/) is a replacement for `ls` written in Rust.
|
||||
|
||||
## Rationale
|
||||
|
||||
**exa** is a modern replacement for the command-line program ls that ships with Unix and Linux operating systems, with more features and better defaults. It uses colours to distinguish file types and metadata. It knows about symlinks, extended attributes, and Git. And it’s **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.
|
||||
|
||||
## Screenshots
|
||||
|
||||
![Screenshots of exa](screenshots.png)
|
||||
@ -9,7 +15,7 @@
|
||||
|
||||
## Options
|
||||
|
||||
exa’s options are similar, but not exactly the same, as `ls`.
|
||||
exa’s options are almost, but not quite, entirely unlike `ls`'s.
|
||||
|
||||
### Display Options
|
||||
|
||||
@ -30,6 +36,8 @@ exa’s options are similar, but not exactly the same, as `ls`.
|
||||
- **-r**, **--reverse**: reverse the sort order
|
||||
- **-s**, **--sort=(field)**: which field to sort by
|
||||
- **--group-directories-first**: list directories before other files
|
||||
- **-D**, **--only-dirs**: list only directories
|
||||
- **--git-ignore**: ignore files mentioned in `.gitignore`
|
||||
- **-I**, **--ignore-glob=(globs)**: glob patterns (pipe-separated) of files to ignore
|
||||
|
||||
Pass the `--all` option twice to also show the `.` and `..` directories.
|
||||
@ -50,35 +58,54 @@ These options are available when running with --long (`-l`):
|
||||
- **-u**, **--accessed**: use the accessed timestamp field
|
||||
- **-U**, **--created**: use the created timestamp field
|
||||
- **-@**, **--extended**: list each file's extended attributes and sizes
|
||||
- **--git**: list each file's Git status, if tracked
|
||||
- **--git**: list each file's Git status, if tracked or ignored
|
||||
- **--time-style**: how to format timestamps
|
||||
|
||||
- Valid **--color** options are **always**, **automatic**, and **never**.
|
||||
- Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter are case-sensitive.
|
||||
- Valid time fields are **modified**, **accessed**, and **created**.
|
||||
- Valid sort fields are **accessed**, **changed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase. The modified field has the aliases **date**, **time**, and **newest**, while its reverse has the aliases **age** and **oldest**.
|
||||
- Valid time fields are **modified**, **changed**, **accessed**, and **created**.
|
||||
- Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
exa is written in [Rust](http://www.rust-lang.org).
|
||||
exa is written in [Rust](http://www.rust-lang.org). You will need rustc version 1.17.0 or higher. The recommended way to install Rust is from the official download page.
|
||||
Once you have it set up, a simple `make install` will compile exa and install it into `/usr/local/bin`.
|
||||
|
||||
exa depends on [libgit2](https://github.com/alexcrichton/git2-rs) for certain features.
|
||||
If you’re unable to compile libgit2, you can opt out of Git support by running `cargo build --release --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: `cargo build --release --target=x86_64-unknown-linux-musl --features vendored-openssl,git`
|
||||
|
||||
### Cargo Install
|
||||
|
||||
If you’re using a recent version of Cargo (0.5.0 or higher), you can use the `cargo install` command:
|
||||
|
||||
cargo install --git https://github.com/ogham/exa
|
||||
cargo install exa
|
||||
|
||||
or:
|
||||
|
||||
cargo install --no-default-features --git https://github.com/ogham/exa
|
||||
cargo install --no-default-features exa
|
||||
|
||||
Cargo will clone the repository to a temporary directory, build it there and place the `exa` binary to: `$HOME/.cargo` (and can be overridden by setting the `--root` option).
|
||||
Cargo will build the `exa` binary and place it in `$HOME/.cargo` (this location can be overridden by setting the `--root` option).
|
||||
|
||||
### Homebrew
|
||||
|
||||
If you're using [homebrew](https://brew.sh/), you can use the `brew install` command:
|
||||
|
||||
brew install exa
|
||||
|
||||
or:
|
||||
|
||||
brew install exa --without-git
|
||||
|
||||
[Formulae](https://github.com/Homebrew/homebrew-core/blob/master/Formula/exa.rb)
|
||||
|
||||
### Nix
|
||||
|
||||
`exa` is also installable through [the derivation](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/exa/default.nix) using the [nix package manager](https://nixos.org/nix/) by running:
|
||||
|
||||
nix-env -i exa
|
||||
|
||||
## Testing with Vagrant
|
||||
|
||||
@ -90,7 +117,7 @@ The initial attempt to solve the problem was just to create a directory of “aw
|
||||
|
||||
An alternative solution is to fake *everything*: create a virtual machine with a known state and run the tests on *that*. This is what Vagrant does. Although it takes a while to download and set up, it gives everyone the same development environment to test for any obvious regressions.
|
||||
|
||||
[Vagrant]: https://www.vagrantup.com/docs/why-vagrant/
|
||||
[Vagrant]: https://www.vagrantup.com/
|
||||
[testing]: https://eev.ee/blog/2016/08/22/testing-for-people-who-hate-testing/#troublesome-cases
|
||||
|
||||
First, initialise the VM:
|
||||
|
568
Vagrantfile
vendored
568
Vagrantfile
vendored
@ -1,82 +1,133 @@
|
||||
require 'date'
|
||||
|
||||
Vagrant.configure(2) do |config|
|
||||
|
||||
# We use Ubuntu instead of Debian because the image comes with two-way
|
||||
# shared folder support by default.
|
||||
UBUNTU = 'ubuntu/xenial64'
|
||||
|
||||
# The main VM is the one used for development and testing.
|
||||
config.vm.define(:exa, primary: true) do |config|
|
||||
config.vm.provider :virtualbox do |v|
|
||||
v.name = 'exa'
|
||||
v.memory = 1024
|
||||
v.cpus = 1
|
||||
v.name = 'exa'
|
||||
v.memory = 1024
|
||||
v.cpus = 1
|
||||
end
|
||||
|
||||
developer = 'ubuntu'
|
||||
|
||||
|
||||
# We use Ubuntu instead of Debian because the image comes with two-way
|
||||
# shared folder support by default.
|
||||
config.vm.box = 'ubuntu/xenial64'
|
||||
config.vm.box = UBUNTU
|
||||
config.vm.hostname = 'exa'
|
||||
|
||||
|
||||
# Make sure we know the VM image’s default user name. The ‘cassowary’ user
|
||||
# (specified later) is used for most of the test *output*, but we still
|
||||
# need to know where the ‘target’ and ‘.cargo’ directories go.
|
||||
developer = 'vagrant'
|
||||
|
||||
|
||||
# Install the dependencies needed for exa to build, as quietly as
|
||||
# apt can do.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
apt-get install -qq -o=Dpkg::Use-Pty=0 -y \
|
||||
git cmake curl attr libgit2-dev \
|
||||
fish zsh bash bash-completion
|
||||
set -xe
|
||||
apt-get update
|
||||
apt-get install -qq -o=Dpkg::Use-Pty=0 -y \
|
||||
git cmake curl attr libgit2-dev zip \
|
||||
fish zsh bash bash-completion
|
||||
EOF
|
||||
|
||||
|
||||
# Guarantee that the timezone is UTC -- some of the tests
|
||||
# depend on this (for now).
|
||||
config.vm.provision :shell, privileged: true, inline:
|
||||
%[timedatectl set-timezone UTC]
|
||||
%[timedatectl set-timezone UTC]
|
||||
|
||||
|
||||
# Install Rust.
|
||||
# This is done as vagrant, not root, because it’s vagrant
|
||||
# who actually uses it. Sent to /dev/null because the progress
|
||||
# bar produces a ton of output.
|
||||
config.vm.provision :shell, privileged: false, inline:
|
||||
%[hash rustc &>/dev/null || curl -sSf https://static.rust-lang.org/rustup.sh | sh &> /dev/null]
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
if hash rustc &>/dev/null; then
|
||||
echo "Rust is already installed"
|
||||
else
|
||||
set -xe
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
fi
|
||||
EOF
|
||||
|
||||
|
||||
# Use a different ‘target’ directory on the VM than on the host.
|
||||
# By default it just uses the one in /vagrant/target, which can
|
||||
# cause problems if it has different permissions than the other
|
||||
# directories, or contains object files compiled for the host.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
function put_line() {
|
||||
grep -q -F "$2" $1 || echo "$2" >> $1
|
||||
}
|
||||
|
||||
put_line ~/.bashrc 'export CARGO_TARGET_DIR=/home/#{developer}/target'
|
||||
put_line ~/.bashrc 'export PATH=$PATH:/home/#{developer}/.cargo/bin'
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/#{developer}/.cargo/bin"' > /etc/environment
|
||||
echo 'CARGO_TARGET_DIR="/home/#{developer}/target"' >> /etc/environment
|
||||
EOF
|
||||
|
||||
|
||||
# Create "dexa" and "rexa" scripts that run the debug and release
|
||||
# compiled versions of exa.
|
||||
# Create a variety of misc scripts.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
set -xe
|
||||
|
||||
echo -e "#!/bin/sh\n/home/#{developer}/target/debug/exa \\$*" > /usr/bin/exa
|
||||
echo -e "#!/bin/sh\n/home/#{developer}/target/release/exa \\$*" > /usr/bin/rexa
|
||||
chmod +x /usr/bin/{exa,rexa}
|
||||
ln -sf /vagrant/devtools/dev-run-debug.sh /usr/bin/exa
|
||||
ln -sf /vagrant/devtools/dev-run-release.sh /usr/bin/rexa
|
||||
|
||||
echo -e "#!/bin/sh\ncargo build --manifest-path /vagrant/Cargo.toml \\$@" > /usr/bin/build-exa
|
||||
ln -sf /usr/bin/build-exa /usr/bin/b
|
||||
|
||||
echo -e "#!/bin/sh\ncargo test --manifest-path /vagrant/Cargo.toml --lib \\$@ -- --quiet" > /usr/bin/test-exa
|
||||
ln -sf /usr/bin/test-exa /usr/bin/t
|
||||
|
||||
echo -e "#!/bin/sh\n/vagrant/xtests/run.sh" > /usr/bin/run-xtests
|
||||
ln -sf /usr/bin/run-xtests /usr/bin/x
|
||||
|
||||
echo -e "#!/bin/sh\nbuild-exa && test-exa && run-xtests" > /usr/bin/compile-exa
|
||||
ln -sf /usr/bin/compile-exa /usr/bin/c
|
||||
|
||||
echo -e "#!/bin/sh\nbash /vagrant/devtools/dev-package-for-linux.sh \\$@" > /usr/bin/package-exa
|
||||
echo -e "#!/bin/sh\ncat /etc/motd" > /usr/bin/halp
|
||||
|
||||
chmod +x /usr/bin/{exa,rexa,b,t,x,c,build-exa,test-exa,run-xtests,compile-exa,package-exa,halp}
|
||||
EOF
|
||||
|
||||
|
||||
# This fix is applied by changing the VM rather than changing the
|
||||
# Cargo.toml file so it works for everyone because it’s such a niche
|
||||
# build issue, it’s not worth specifying a non-crates.io dependency
|
||||
# and losing the ability to `cargo publish` the exa crate there!
|
||||
# It also isolates the hackiness to the one place I can test it
|
||||
# actually works.
|
||||
|
||||
|
||||
# Configure the welcoming text that gets shown.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
rm -f /etc/update-motd.d/*
|
||||
|
||||
# Capture the help text so it gets displayed first
|
||||
bash /vagrant/devtools/dev-help.sh > /etc/motd
|
||||
|
||||
# Tell bash to execute a bunch of stuff when a session starts
|
||||
echo "source /vagrant/devtools/dev-bash.sh" > /home/#{developer}/.bash_profile
|
||||
chown #{developer} /home/#{developer}/.bash_profile
|
||||
|
||||
# Disable last login date in sshd
|
||||
sed -i '/PrintLastLog yes/c\PrintLastLog no' /etc/ssh/sshd_config
|
||||
systemctl restart sshd
|
||||
EOF
|
||||
|
||||
|
||||
# Link the completion files so they’re “installed”.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
set -xe
|
||||
|
||||
test -h /etc/bash_completion.d/exa \
|
||||
|| ln -s /vagrant/contrib/completions.bash /etc/bash_completion.d/exa
|
||||
test -h /etc/bash_completion.d/exa \
|
||||
|| ln -s /vagrant/contrib/completions.bash /etc/bash_completion.d/exa
|
||||
|
||||
test -h /usr/share/zsh/vendor-completions/_exa \
|
||||
|| ln -s /vagrant/contrib/completions.zsh /usr/share/zsh/vendor-completions/_exa
|
||||
test -h /usr/share/zsh/vendor-completions/_exa \
|
||||
|| ln -s /vagrant/contrib/completions.zsh /usr/share/zsh/vendor-completions/_exa
|
||||
|
||||
test -h /usr/share/fish/completions/exa.fish \
|
||||
|| ln -s /vagrant/contrib/completions.fish /usr/share/fish/completions/exa.fish
|
||||
test -h /usr/share/fish/completions/exa.fish \
|
||||
|| ln -s /vagrant/contrib/completions.fish /usr/share/fish/completions/exa.fish
|
||||
EOF
|
||||
|
||||
|
||||
@ -85,7 +136,7 @@ Vagrant.configure(2) do |config|
|
||||
# test outputs to depend on “vagrant” or “ubuntu” existing.
|
||||
user = "cassowary"
|
||||
config.vm.provision :shell, privileged: true, inline:
|
||||
%[id -u #{user} &>/dev/null || useradd #{user}]
|
||||
%[id -u #{user} &>/dev/null || useradd #{user}]
|
||||
|
||||
|
||||
# The second one has a long name, to test that the file owner column
|
||||
@ -93,7 +144,7 @@ Vagrant.configure(2) do |config|
|
||||
# set this up on the *actual* system!
|
||||
longuser = "antidisestablishmentarienism"
|
||||
config.vm.provision :shell, privileged: true, inline:
|
||||
%[id -u #{longuser} &>/dev/null || useradd #{longuser}]
|
||||
%[id -u #{longuser} &>/dev/null || useradd #{longuser}]
|
||||
|
||||
|
||||
# Because the timestamps are formatted differently depending on whether
|
||||
@ -114,27 +165,27 @@ Vagrant.configure(2) do |config|
|
||||
# directory to house new ones.
|
||||
test_dir = "/testcases"
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
rm -rfv #{test_dir}
|
||||
mkdir #{test_dir}
|
||||
chmod 777 #{test_dir}
|
||||
set -xe
|
||||
rm -rfv #{test_dir}
|
||||
mkdir #{test_dir}
|
||||
chmod 777 #{test_dir}
|
||||
EOF
|
||||
|
||||
|
||||
# Awkward file size testcases.
|
||||
# This needs sudo to set the files’ users at the very end.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/files"
|
||||
for i in {1..13}; do
|
||||
fallocate -l "$i" "#{test_dir}/files/$i"_bytes
|
||||
fallocate -l "$i"KiB "#{test_dir}/files/$i"_KiB
|
||||
fallocate -l "$i"MiB "#{test_dir}/files/$i"_MiB
|
||||
done
|
||||
set -xe
|
||||
mkdir "#{test_dir}/files"
|
||||
for i in {1..13}; do
|
||||
fallocate -l "$i" "#{test_dir}/files/$i"_bytes
|
||||
fallocate -l "$i"KiB "#{test_dir}/files/$i"_KiB
|
||||
fallocate -l "$i"MiB "#{test_dir}/files/$i"_MiB
|
||||
done
|
||||
|
||||
touch -t #{some_date} "#{test_dir}/files/"*
|
||||
chmod 644 "#{test_dir}/files/"*
|
||||
sudo chown #{user}:#{user} "#{test_dir}/files/"*
|
||||
touch -t #{some_date} "#{test_dir}/files/"*
|
||||
chmod 644 "#{test_dir}/files/"*
|
||||
sudo chown #{user}:#{user} "#{test_dir}/files/"*
|
||||
EOF
|
||||
|
||||
|
||||
@ -142,41 +193,44 @@ Vagrant.configure(2) do |config|
|
||||
# These aren’t tested in details view, but we set timestamps on them to
|
||||
# test that various sort options work.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/file-names-exts"
|
||||
set -xe
|
||||
mkdir "#{test_dir}/file-names-exts"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/Makefile"
|
||||
touch "#{test_dir}/file-names-exts/Makefile"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/IMAGE.PNG"
|
||||
touch "#{test_dir}/file-names-exts/image.svg"
|
||||
touch "#{test_dir}/file-names-exts/IMAGE.PNG"
|
||||
touch "#{test_dir}/file-names-exts/image.svg"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/VIDEO.AVI"
|
||||
touch "#{test_dir}/file-names-exts/video.wmv"
|
||||
touch "#{test_dir}/file-names-exts/VIDEO.AVI"
|
||||
touch "#{test_dir}/file-names-exts/video.wmv"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/music.mp3"
|
||||
touch "#{test_dir}/file-names-exts/MUSIC.OGG"
|
||||
touch "#{test_dir}/file-names-exts/music.mp3"
|
||||
touch "#{test_dir}/file-names-exts/MUSIC.OGG"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/lossless.flac"
|
||||
touch "#{test_dir}/file-names-exts/lossless.wav"
|
||||
touch "#{test_dir}/file-names-exts/lossless.flac"
|
||||
touch "#{test_dir}/file-names-exts/lossless.wav"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/crypto.asc"
|
||||
touch "#{test_dir}/file-names-exts/crypto.signature"
|
||||
touch "#{test_dir}/file-names-exts/crypto.asc"
|
||||
touch "#{test_dir}/file-names-exts/crypto.signature"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/document.pdf"
|
||||
touch "#{test_dir}/file-names-exts/DOCUMENT.XLSX"
|
||||
touch "#{test_dir}/file-names-exts/document.pdf"
|
||||
touch "#{test_dir}/file-names-exts/DOCUMENT.XLSX"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/COMPRESSED.ZIP"
|
||||
touch "#{test_dir}/file-names-exts/compressed.tar.gz"
|
||||
touch "#{test_dir}/file-names-exts/compressed.tgz"
|
||||
touch "#{test_dir}/file-names-exts/COMPRESSED.ZIP"
|
||||
touch "#{test_dir}/file-names-exts/compressed.tar.gz"
|
||||
touch "#{test_dir}/file-names-exts/compressed.tgz"
|
||||
touch "#{test_dir}/file-names-exts/compressed.tar.xz"
|
||||
touch "#{test_dir}/file-names-exts/compressed.txz"
|
||||
touch "#{test_dir}/file-names-exts/compressed.deb"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/backup~"
|
||||
touch "#{test_dir}/file-names-exts/#SAVEFILE#"
|
||||
touch "#{test_dir}/file-names-exts/file.tmp"
|
||||
touch "#{test_dir}/file-names-exts/backup~"
|
||||
touch "#{test_dir}/file-names-exts/#SAVEFILE#"
|
||||
touch "#{test_dir}/file-names-exts/file.tmp"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/compiled.class"
|
||||
touch "#{test_dir}/file-names-exts/compiled.o"
|
||||
touch "#{test_dir}/file-names-exts/compiled.js"
|
||||
touch "#{test_dir}/file-names-exts/compiled.coffee"
|
||||
touch "#{test_dir}/file-names-exts/compiled.class"
|
||||
touch "#{test_dir}/file-names-exts/compiled.o"
|
||||
touch "#{test_dir}/file-names-exts/compiled.js"
|
||||
touch "#{test_dir}/file-names-exts/compiled.coffee"
|
||||
EOF
|
||||
|
||||
|
||||
@ -188,70 +242,70 @@ Vagrant.configure(2) do |config|
|
||||
# will interpolate them instead of bash, but because Vagrant prints out
|
||||
# each command it runs, your *own* terminal will go “ding” from the alarm!
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/file-names"
|
||||
set -xe
|
||||
mkdir "#{test_dir}/file-names"
|
||||
|
||||
echo -ne "#{test_dir}/file-names/ascii: hello" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/emoji: [🆒]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/utf-8: pâté" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/ascii: hello" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/emoji: [🆒]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/utf-8: pâté" | xargs -0 touch
|
||||
|
||||
echo -ne "#{test_dir}/file-names/bell: [\\a]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/backspace: [\\b]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/form-feed: [\\f]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/new-line: [\\n]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/return: [\\r]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/tab: [\\t]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/vertical-tab: [\\v]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/bell: [\\a]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/backspace: [\\b]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/form-feed: [\\f]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/new-line: [\\n]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/return: [\\r]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/tab: [\\t]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/vertical-tab: [\\v]" | xargs -0 touch
|
||||
|
||||
echo -ne "#{test_dir}/file-names/escape: [\\033]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/ansi: [\\033[34mblue\\033[0m]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/escape: [\\033]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/ansi: [\\033[34mblue\\033[0m]" | xargs -0 touch
|
||||
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-1: [\\xFF]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-2: [\\xc3\\x28]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-3: [\\xe2\\x82\\x28]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-4: [\\xf0\\x28\\x8c\\x28]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-1: [\\xFF]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-2: [\\xc3\\x28]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-3: [\\xe2\\x82\\x28]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-4: [\\xf0\\x28\\x8c\\x28]" | xargs -0 touch
|
||||
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]" | xargs -0 mkdir
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/subfile" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/another: [\\n]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]" | xargs -0 mkdir
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/subfile" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/another: [\\n]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken" | xargs -0 touch
|
||||
|
||||
mkdir "#{test_dir}/file-names/links"
|
||||
ln -s "#{test_dir}/file-names/new-line-dir"*/* "#{test_dir}/file-names/links"
|
||||
mkdir "#{test_dir}/file-names/links"
|
||||
ln -s "#{test_dir}/file-names/new-line-dir"*/* "#{test_dir}/file-names/links"
|
||||
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken" | xargs -0 rm
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken" | xargs -0 rm
|
||||
EOF
|
||||
|
||||
|
||||
# Special file testcases.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/specials"
|
||||
set -xe
|
||||
mkdir "#{test_dir}/specials"
|
||||
|
||||
sudo mknod "#{test_dir}/specials/block-device" b 3 60
|
||||
sudo mknod "#{test_dir}/specials/char-device" c 14 40
|
||||
sudo mknod "#{test_dir}/specials/named-pipe" p
|
||||
sudo mknod "#{test_dir}/specials/block-device" b 3 60
|
||||
sudo mknod "#{test_dir}/specials/char-device" c 14 40
|
||||
sudo mknod "#{test_dir}/specials/named-pipe" p
|
||||
|
||||
sudo touch -t #{some_date} "#{test_dir}/specials/"*
|
||||
sudo touch -t #{some_date} "#{test_dir}/specials/"*
|
||||
EOF
|
||||
|
||||
|
||||
# Awkward symlink testcases.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/links"
|
||||
set -xe
|
||||
mkdir "#{test_dir}/links"
|
||||
|
||||
ln -s / "#{test_dir}/links/root"
|
||||
ln -s /usr "#{test_dir}/links/usr"
|
||||
ln -s nowhere "#{test_dir}/links/broken"
|
||||
ln -s /proc/1/root "#{test_dir}/links/forbidden"
|
||||
ln -s / "#{test_dir}/links/root"
|
||||
ln -s /usr "#{test_dir}/links/usr"
|
||||
ln -s nowhere "#{test_dir}/links/broken"
|
||||
ln -s /proc/1/root "#{test_dir}/links/forbidden"
|
||||
|
||||
touch "#{test_dir}/links/some_file"
|
||||
ln -s "#{test_dir}/links/some_file" "#{test_dir}/links/some_file_absolute"
|
||||
(cd "#{test_dir}/links"; ln -s "some_file" "some_file_relative")
|
||||
(cd "#{test_dir}/links"; ln -s "." "current_dir")
|
||||
(cd "#{test_dir}/links"; ln -s ".." "parent_dir")
|
||||
(cd "#{test_dir}/links"; ln -s "itself" "itself")
|
||||
touch "#{test_dir}/links/some_file"
|
||||
ln -s "#{test_dir}/links/some_file" "#{test_dir}/links/some_file_absolute"
|
||||
(cd "#{test_dir}/links"; ln -s "some_file" "some_file_relative")
|
||||
(cd "#{test_dir}/links"; ln -s "." "current_dir")
|
||||
(cd "#{test_dir}/links"; ln -s ".." "parent_dir")
|
||||
(cd "#{test_dir}/links"; ln -s "itself" "itself")
|
||||
EOF
|
||||
|
||||
|
||||
@ -260,16 +314,16 @@ Vagrant.configure(2) do |config|
|
||||
# of the groups (because they don’t exist), and chown and chgrp
|
||||
# are smart enough to disallow it!
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/passwd"
|
||||
set -xe
|
||||
mkdir "#{test_dir}/passwd"
|
||||
|
||||
touch -t #{some_date} "#{test_dir}/passwd/unknown-uid"
|
||||
chmod 644 "#{test_dir}/passwd/unknown-uid"
|
||||
sudo chown #{invalid_uid}:#{user} "#{test_dir}/passwd/unknown-uid"
|
||||
touch -t #{some_date} "#{test_dir}/passwd/unknown-uid"
|
||||
chmod 644 "#{test_dir}/passwd/unknown-uid"
|
||||
sudo chown #{invalid_uid}:#{user} "#{test_dir}/passwd/unknown-uid"
|
||||
|
||||
touch -t #{some_date} "#{test_dir}/passwd/unknown-gid"
|
||||
chmod 644 "#{test_dir}/passwd/unknown-gid"
|
||||
sudo chown #{user}:#{invalid_gid} "#{test_dir}/passwd/unknown-gid"
|
||||
touch -t #{some_date} "#{test_dir}/passwd/unknown-gid"
|
||||
chmod 644 "#{test_dir}/passwd/unknown-gid"
|
||||
sudo chown #{user}:#{invalid_gid} "#{test_dir}/passwd/unknown-gid"
|
||||
EOF
|
||||
|
||||
|
||||
@ -278,26 +332,25 @@ Vagrant.configure(2) do |config|
|
||||
# when you don’t already own the file mean that we need to use ‘sudo’
|
||||
# to change permissions to those.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/permissions"
|
||||
set -xe
|
||||
mkdir "#{test_dir}/permissions"
|
||||
|
||||
mkdir "#{test_dir}/permissions/forbidden-directory"
|
||||
chmod 000 "#{test_dir}/permissions/forbidden-directory"
|
||||
touch -t #{some_date} "#{test_dir}/permissions/forbidden-directory"
|
||||
sudo chown #{user}:#{user} "#{test_dir}/permissions/forbidden-directory"
|
||||
|
||||
for perms in 000 001 002 004 010 020 040 100 200 400 644 755 777 1000 1001 2000 2010 4000 4100 7666 7777; do
|
||||
touch "#{test_dir}/permissions/$perms"
|
||||
sudo chown #{user}:#{user} "#{test_dir}/permissions/$perms"
|
||||
sudo chmod $perms "#{test_dir}/permissions/$perms"
|
||||
sudo touch -t #{some_date} "#{test_dir}/permissions/$perms"
|
||||
done
|
||||
mkdir "#{test_dir}/permissions/forbidden-directory"
|
||||
chmod 000 "#{test_dir}/permissions/forbidden-directory"
|
||||
touch -t #{some_date} "#{test_dir}/permissions/forbidden-directory"
|
||||
sudo chown #{user}:#{user} "#{test_dir}/permissions/forbidden-directory"
|
||||
|
||||
for perms in 000 001 002 004 010 020 040 100 200 400 644 755 777 1000 1001 2000 2010 4000 4100 7666 7777; do
|
||||
touch "#{test_dir}/permissions/$perms"
|
||||
sudo chown #{user}:#{user} "#{test_dir}/permissions/$perms"
|
||||
sudo chmod $perms "#{test_dir}/permissions/$perms"
|
||||
sudo touch -t #{some_date} "#{test_dir}/permissions/$perms"
|
||||
done
|
||||
EOF
|
||||
|
||||
old = '200303030000.00'
|
||||
med = '200606152314.29'
|
||||
new = '200907221038.53'
|
||||
med = '200606152314.29' # the june gets used for fr_FR locale tests
|
||||
new = '200912221038.53' # and the december for ja_JP local tests
|
||||
|
||||
# Awkward date and time testcases.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
@ -333,116 +386,203 @@ Vagrant.configure(2) do |config|
|
||||
# “one-file” in their name, then just give the right number of
|
||||
# xattrs and children to those.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/attributes"
|
||||
set -xe
|
||||
mkdir "#{test_dir}/attributes"
|
||||
|
||||
mkdir "#{test_dir}/attributes/files"
|
||||
touch "#{test_dir}/attributes/files/"{no-xattrs,one-xattr,two-xattrs}{,_forbidden}
|
||||
mkdir "#{test_dir}/attributes/files"
|
||||
touch "#{test_dir}/attributes/files/"{no-xattrs,one-xattr,two-xattrs}{,_forbidden}
|
||||
|
||||
mkdir "#{test_dir}/attributes/dirs"
|
||||
mkdir "#{test_dir}/attributes/dirs/"{no-xattrs,one-xattr,two-xattrs}_{empty,one-file,two-files}{,_forbidden}
|
||||
mkdir "#{test_dir}/attributes/dirs"
|
||||
mkdir "#{test_dir}/attributes/dirs/"{no-xattrs,one-xattr,two-xattrs}_{empty,one-file,two-files}{,_forbidden}
|
||||
|
||||
setfattr -n user.greeting -v hello "#{test_dir}/attributes"/**/*{one-xattr,two-xattrs}*
|
||||
setfattr -n user.another_greeting -v hi "#{test_dir}/attributes"/**/*two-xattrs*
|
||||
setfattr -n user.greeting -v hello "#{test_dir}/attributes"/**/*{one-xattr,two-xattrs}*
|
||||
setfattr -n user.another_greeting -v hi "#{test_dir}/attributes"/**/*two-xattrs*
|
||||
|
||||
for dir in "#{test_dir}/attributes/dirs/"*one-file*; do
|
||||
touch $dir/file-in-question
|
||||
done
|
||||
for dir in "#{test_dir}/attributes/dirs/"*one-file*; do
|
||||
touch $dir/file-in-question
|
||||
done
|
||||
|
||||
for dir in "#{test_dir}/attributes/dirs/"*two-files*; do
|
||||
touch $dir/this-file
|
||||
touch $dir/that-file
|
||||
done
|
||||
for dir in "#{test_dir}/attributes/dirs/"*two-files*; do
|
||||
touch $dir/this-file
|
||||
touch $dir/that-file
|
||||
done
|
||||
|
||||
touch -t #{some_date} "#{test_dir}/attributes" # there's probably
|
||||
touch -t #{some_date} "#{test_dir}/attributes"/* # a better
|
||||
touch -t #{some_date} "#{test_dir}/attributes"/*/* # way to
|
||||
touch -t #{some_date} "#{test_dir}/attributes"/*/*/* # do this
|
||||
find "#{test_dir}/attributes" -exec touch {} -t #{some_date} \\;
|
||||
|
||||
# I want to use the following to test,
|
||||
# but it only works on macos:
|
||||
#chmod +a "#{user} deny readextattr" "#{test_dir}/attributes"/**/*_forbidden
|
||||
# I want to use the following to test,
|
||||
# but it only works on macos:
|
||||
#chmod +a "#{user} deny readextattr" "#{test_dir}/attributes"/**/*_forbidden
|
||||
|
||||
sudo chmod 000 "#{test_dir}/attributes"/**/*_forbidden
|
||||
sudo chown #{user}:#{user} -R "#{test_dir}/attributes"
|
||||
sudo chmod 000 "#{test_dir}/attributes"/**/*_forbidden
|
||||
sudo chown #{user}:#{user} -R "#{test_dir}/attributes"
|
||||
EOF
|
||||
|
||||
|
||||
# A sample Git repository
|
||||
# This uses cd because it's easier than telling Git where to go each time
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/git"
|
||||
cd "#{test_dir}/git"
|
||||
git init
|
||||
set -xe
|
||||
mkdir "#{test_dir}/git"
|
||||
cd "#{test_dir}/git"
|
||||
git init
|
||||
|
||||
mkdir edits additions moves
|
||||
mkdir edits additions moves
|
||||
|
||||
echo "original content" | tee edits/{staged,unstaged,both}
|
||||
echo "this file gets moved" > moves/hither
|
||||
echo "original content" | tee edits/{staged,unstaged,both}
|
||||
echo "this file gets moved" > moves/hither
|
||||
|
||||
git add edits moves
|
||||
git commit -m "Automated test commit"
|
||||
git add edits moves
|
||||
git config --global user.email "exa@exa.exa"
|
||||
git config --global user.name "Exa Exa"
|
||||
git commit -m "Automated test commit"
|
||||
|
||||
echo "modifications!" | tee edits/{staged,both}
|
||||
touch additions/{staged,edited}
|
||||
mv moves/{hither,thither}
|
||||
|
||||
echo "modifications!" | tee edits/{staged,both}
|
||||
touch additions/{staged,edited}
|
||||
mv moves/{hither,thither}
|
||||
git add edits moves additions
|
||||
echo "more modifications!" | tee edits/unstaged edits/both additions/edited
|
||||
touch additions/unstaged
|
||||
|
||||
git add edits moves additions
|
||||
echo "more modifications!" | tee edits/unstaged edits/both additions/edited
|
||||
touch additions/unstaged
|
||||
|
||||
|
||||
touch -t #{some_date} "#{test_dir}/git/"*/*
|
||||
sudo chown #{user}:#{user} -R "#{test_dir}/git"
|
||||
find "#{test_dir}/git" -exec touch {} -t #{some_date} \\;
|
||||
sudo chown #{user}:#{user} -R "#{test_dir}/git"
|
||||
EOF
|
||||
|
||||
|
||||
# A second Git repository
|
||||
# for testing two at once
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir -p "#{test_dir}/git2/deeply/nested/directory"
|
||||
cd "#{test_dir}/git2"
|
||||
git init
|
||||
|
||||
touch "deeply/nested/directory/upd8d"
|
||||
git add "deeply/nested/directory/upd8d"
|
||||
git commit -m "Automated test commit"
|
||||
|
||||
echo "Now with contents" > "deeply/nested/directory/upd8d"
|
||||
touch "deeply/nested/directory/l8st"
|
||||
|
||||
echo -e "target\n*.mp3" > ".gitignore"
|
||||
mkdir "ignoreds"
|
||||
touch "ignoreds/music.mp3"
|
||||
touch "ignoreds/music.m4a"
|
||||
mkdir "ignoreds/nested"
|
||||
touch "ignoreds/nested/70s grove.mp3"
|
||||
touch "ignoreds/nested/funky chicken.m4a"
|
||||
|
||||
mkdir "target"
|
||||
touch "target/another ignored file"
|
||||
|
||||
mkdir "deeply/nested/repository"
|
||||
cd "deeply/nested/repository"
|
||||
git init
|
||||
touch subfile
|
||||
|
||||
find "#{test_dir}/git2" -exec touch {} -t #{some_date} \\;
|
||||
sudo chown #{user}:#{user} -R "#{test_dir}/git2"
|
||||
EOF
|
||||
|
||||
# Hidden and dot file testcases.
|
||||
# We need to set the permissions of `.` and `..` because they actually
|
||||
# get displayed in the output here, so this has to come last.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
shopt -u dotglob
|
||||
GLOBIGNORE=".:.."
|
||||
set -xe
|
||||
shopt -u dotglob
|
||||
GLOBIGNORE=".:.."
|
||||
|
||||
mkdir "#{test_dir}/hiddens"
|
||||
touch "#{test_dir}/hiddens/visible"
|
||||
touch "#{test_dir}/hiddens/.hidden"
|
||||
touch "#{test_dir}/hiddens/..extra-hidden"
|
||||
mkdir "#{test_dir}/hiddens"
|
||||
touch "#{test_dir}/hiddens/visible"
|
||||
touch "#{test_dir}/hiddens/.hidden"
|
||||
touch "#{test_dir}/hiddens/..extra-hidden"
|
||||
|
||||
# ./hiddens/
|
||||
touch -t #{some_date} "#{test_dir}/hiddens/"*
|
||||
chmod 644 "#{test_dir}/hiddens/"*
|
||||
sudo chown #{user}:#{user} "#{test_dir}/hiddens/"*
|
||||
# ./hiddens/
|
||||
touch -t #{some_date} "#{test_dir}/hiddens/"*
|
||||
chmod 644 "#{test_dir}/hiddens/"*
|
||||
sudo chown #{user}:#{user} "#{test_dir}/hiddens/"*
|
||||
|
||||
# .
|
||||
touch -t #{some_date} "#{test_dir}/hiddens"
|
||||
chmod 755 "#{test_dir}/hiddens"
|
||||
sudo chown #{user}:#{user} "#{test_dir}/hiddens"
|
||||
# .
|
||||
touch -t #{some_date} "#{test_dir}/hiddens"
|
||||
chmod 755 "#{test_dir}/hiddens"
|
||||
sudo chown #{user}:#{user} "#{test_dir}/hiddens"
|
||||
|
||||
# ..
|
||||
sudo touch -t #{some_date} "#{test_dir}"
|
||||
sudo chmod 755 "#{test_dir}"
|
||||
sudo chown #{user}:#{user} "#{test_dir}"
|
||||
# ..
|
||||
sudo touch -t #{some_date} "#{test_dir}"
|
||||
sudo chmod 755 "#{test_dir}"
|
||||
sudo chown #{user}:#{user} "#{test_dir}"
|
||||
EOF
|
||||
|
||||
|
||||
# Set up some locales
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
|
||||
# uncomment these from the config file
|
||||
sudo sed -i '/fr_FR.UTF-8/s/^# //g' /etc/locale.gen
|
||||
sudo sed -i '/ja_JP.UTF-8/s/^# //g' /etc/locale.gen
|
||||
sudo locale-gen
|
||||
EOF
|
||||
|
||||
|
||||
# Install kcov for test coverage
|
||||
# This doesn’t run coverage over the xtests so it’s less useful for now
|
||||
if ENV.key?('INSTALL_KCOV')
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
|
||||
test -e ~/.cargo/bin/cargo-kcov \
|
||||
|| cargo install cargo-kcov
|
||||
test -e ~/.cargo/bin/cargo-kcov \
|
||||
|| cargo install cargo-kcov
|
||||
|
||||
sudo apt-get install -qq -o=Dpkg::Use-Pty=0 -y \
|
||||
cmake g++ pkg-config \
|
||||
libcurl4-openssl-dev libdw-dev binutils-dev libiberty-dev
|
||||
sudo apt-get install -qq -o=Dpkg::Use-Pty=0 -y \
|
||||
cmake g++ pkg-config \
|
||||
libcurl4-openssl-dev libdw-dev binutils-dev libiberty-dev
|
||||
|
||||
cargo kcov --print-install-kcov-sh | sudo sh
|
||||
EOF
|
||||
cargo kcov --print-install-kcov-sh | sudo sh
|
||||
EOF
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Remember that problem that exa had where the binary wasn’t actually
|
||||
# self-contained? Or the problem where the Linux binary was actually the
|
||||
# macOS binary in disguise?
|
||||
#
|
||||
# This is a “fresh” VM that intentionally downloads no dependencies and
|
||||
# installs nothing so that we can check that exa still runs!
|
||||
config.vm.define(:fresh) do |config|
|
||||
config.vm.box = UBUNTU
|
||||
config.vm.hostname = 'fresh'
|
||||
|
||||
config.vm.provider :virtualbox do |v|
|
||||
v.name = 'exa-fresh'
|
||||
v.memory = 384
|
||||
v.cpus = 1
|
||||
end
|
||||
|
||||
# Well, we do need *one* dependency...
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
apt-get install -qq -o=Dpkg::Use-Pty=0 -y unzip
|
||||
EOF
|
||||
|
||||
# This thing also has its own welcoming text.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
rm -f /etc/update-motd.d/*
|
||||
|
||||
# Capture the help text so it gets displayed first
|
||||
bash /vagrant/devtools/dev-help-testvm.sh > /etc/motd
|
||||
|
||||
# Disable last login date in sshd
|
||||
sed -i '/PrintLastLog yes/c\PrintLastLog no' /etc/ssh/sshd_config
|
||||
systemctl restart sshd
|
||||
EOF
|
||||
|
||||
# Make the checker script a command.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
echo -e "#!/bin/sh\nbash /vagrant/devtools/dev-download-and-check-release.sh \"\\$*\"" > /usr/bin/check-release
|
||||
chmod +x /usr/bin/check-release
|
||||
EOF
|
||||
end
|
||||
end
|
||||
|
61
build.rs
Normal file
61
build.rs
Normal file
@ -0,0 +1,61 @@
|
||||
/// The version string isn’t the simplest: we want to show the version,
|
||||
/// current Git hash, and compilation date when building *debug* versions, but
|
||||
/// just the version for *release* versions so the builds are reproducible.
|
||||
///
|
||||
/// This script generates the string from the environment variables that Cargo
|
||||
/// adds (http://doc.crates.io/environment-variables.html) and runs `git` to
|
||||
/// get the SHA1 hash. It then writes the string into a file, which exa then
|
||||
/// includes at build-time.
|
||||
///
|
||||
/// - https://stackoverflow.com/q/43753491/3484614
|
||||
/// - https://crates.io/crates/vergen
|
||||
|
||||
extern crate datetime;
|
||||
use std::io::Result as IOResult;
|
||||
use std::env;
|
||||
|
||||
fn git_hash() -> String {
|
||||
use std::process::Command;
|
||||
|
||||
String::from_utf8_lossy(
|
||||
&Command::new("git")
|
||||
.args(&["rev-parse", "--short", "HEAD"])
|
||||
.output().unwrap()
|
||||
.stdout).trim().to_string()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
write_statics().unwrap();
|
||||
}
|
||||
|
||||
fn is_development_version() -> bool {
|
||||
// Both weekly releases and actual releases are --release releases,
|
||||
// but actual releases will have a proper version number
|
||||
cargo_version().ends_with("-pre") || env::var("PROFILE").unwrap() == "debug"
|
||||
}
|
||||
|
||||
fn cargo_version() -> String {
|
||||
env::var("CARGO_PKG_VERSION").unwrap()
|
||||
}
|
||||
|
||||
fn build_date() -> String {
|
||||
use datetime::{LocalDateTime, ISO};
|
||||
|
||||
let now = LocalDateTime::now();
|
||||
format!("{}", now.date().iso())
|
||||
}
|
||||
|
||||
fn write_statics() -> IOResult<()> {
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
let ver = match is_development_version() {
|
||||
true => format!("exa v{} ({} built on {})", cargo_version(), git_hash(), build_date()),
|
||||
false => format!("exa v{}", cargo_version()),
|
||||
};
|
||||
|
||||
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let mut f = File::create(&out.join("version_string.txt"))?;
|
||||
write!(f, "{:?}", ver)
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
# Maintainer: tuftedocelot@fastmail.fm
|
||||
_pkgname=exa
|
||||
pkgname=${_pkgname}-git
|
||||
pkgver=303
|
||||
pkgrel=1
|
||||
pkgdesc='Replacement for ls written in Rust.'
|
||||
arch=('i686' 'x86_64')
|
||||
url="https://github.com/ogham/exa"
|
||||
license='MIT'
|
||||
depends=('libssh2')
|
||||
makedepends=('cmake' 'rust-nightly-bin' 'libgit2')
|
||||
source=("git+https://github.com/ogham/exa.git")
|
||||
md5sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd "$srcdir/$_pkgname"
|
||||
git rev-list --count HEAD
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir/$_pkgname"
|
||||
make
|
||||
install -Dm755 "$srcdir/$_pkgname/target/release/$_pkgname" "$pkgdir/usr/bin/$_pkgname"
|
||||
install -Dm644 "$srcdir/$_pkgname/man/$_pkgname.1" "$pkgdir/usr/share/man/man1/$_pkgname.1"
|
||||
}
|
@ -14,12 +14,12 @@ _exa()
|
||||
;;
|
||||
|
||||
-s|--sort)
|
||||
COMPREPLY=( $( compgen -W 'name filename Name Filename size filesize extension Extension modified accessed created type inode none --' -- "$cur" ) )
|
||||
COMPREPLY=( $( compgen -W 'name filename Name Filename size filesize extension Extension date time modified changed accessed created type inode oldest newest age none --' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
|
||||
-t|--time)
|
||||
COMPREPLY=( $( compgen -W 'accessed modified created --' -- $cur ) )
|
||||
COMPREPLY=( $( compgen -W 'modified changed accessed created --' -- $cur ) )
|
||||
return
|
||||
;;
|
||||
|
||||
|
24
contrib/completions.fish
Normal file → Executable file
24
contrib/completions.fish
Normal file → Executable file
@ -14,28 +14,36 @@ complete -c exa -l 'color' -d "When to use terminal colours"
|
||||
complete -c exa -l 'colour' -d "When to use terminal colours"
|
||||
complete -c exa -l 'color-scale' -d "Highlight levels of file sizes distinctly"
|
||||
complete -c exa -l 'colour-scale' -d "Highlight levels of file sizes distinctly"
|
||||
complete -c exa -l 'icons' -d "Display icons"
|
||||
|
||||
# Filtering and sorting options
|
||||
complete -c exa -l 'group-directories-first' -d "Sort directories before other files"
|
||||
complete -c exa -s 'a' -l 'all' -d "Show and 'dot' 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" -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' -x -d "Which field to sort by" -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 (case-insensitive)'
|
||||
Ext\t'Sort by file extension (uppercase first)'
|
||||
extension\t'Sort by file extension'
|
||||
Extension\t'Sort by file extension (case-insensitive)'
|
||||
Extension\t'Sort by file extension (uppercase first)'
|
||||
filename\t'Sort by filename'
|
||||
Filename\t'Sort by filename (case-insensitive)'
|
||||
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 (case-insensitive)'
|
||||
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'
|
||||
"
|
||||
|
||||
@ -48,13 +56,15 @@ 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 'm' -l 'modified' -d "Use the modified timestamp field"
|
||||
complete -c exa -s 'S' -l 'blocks' -d "List each file's number of filesystem blocks"
|
||||
complete -c exa -s 't' -l 'time' -x -d "Which timestamp field to list" -a "
|
||||
modified\t'Display modified time'
|
||||
changed\t'Display changed time'
|
||||
accessed\t'Display accessed time'
|
||||
created\t'Display created time'
|
||||
modified\t'Display modified time'
|
||||
"
|
||||
complete -c exa -s 'm' -l 'modified' -d "Use the modified timestamp field"
|
||||
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' -x -d "How to format timestamps" -a "
|
||||
|
@ -1,9 +1,17 @@
|
||||
#compdef exa
|
||||
|
||||
# Save this file as _exa 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() {
|
||||
_arguments \
|
||||
"(- 1 *)"{-v,--version}"[Show version of exa]" \
|
||||
"(- 1 *)"{-\?,--help}"[Show list of command-line options]" \
|
||||
# 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`.
|
||||
_arguments -s -S \
|
||||
"(- *)"{-v,--version}"[Show version of exa]" \
|
||||
"(- *)"{-'\?',--help}"[Show list of command-line options]" \
|
||||
{-1,--oneline}"[Display one entry per line]" \
|
||||
{-l,--long}"[Display extended file metadata as a table]" \
|
||||
{-G,--grid}"[Display entries as a grid]" \
|
||||
@ -11,14 +19,16 @@ __exa() {
|
||||
{-R,--recurse}"[Recurse into directories]" \
|
||||
{-T,--tree}"[Recurse into directories as a tree]" \
|
||||
{-F,--classify}"[Display type indicator by file names]" \
|
||||
{--color,--colour}"[When to use terminal colours]" \
|
||||
{--color,--colour}-scale"[Highlight levels of file sizes distinctly]" \
|
||||
--colo{,u}r"[When to use terminal colours]" \
|
||||
--colo{,u}r-scale"[Highlight levels of file sizes distinctly]" \
|
||||
--icons"[Display icons]" \
|
||||
--group-directories-first"[Sort directories before other files]" \
|
||||
--git-ignore"[Ignore files mentioned in '.gitignore']" \
|
||||
{-a,--all}"[Show hidden and 'dot' files]" \
|
||||
{-d,--list-dirs}"[List directories like regular files]" \
|
||||
{-L,--level}"+[Limit the depth of recursion]" \
|
||||
{-r,--reverse}"[Reverse the sort order]" \
|
||||
{-s,--sort}"[Which field to sort by]:(sort field):(accessed created extension Extension filename Filename inode modified name Name none size type)" \
|
||||
{-s,--sort}="[Which field to sort by]:(sort field):(accessed age changed created date extension Extension filename Filename inode modified oldest name Name newest none size time type)" \
|
||||
{-I,--ignore-glob}"[Ignore files that match these glob patterns]" \
|
||||
{-b,--binary}"[List file sizes with binary prefixes]" \
|
||||
{-B,--bytes}"[List file sizes in bytes, without any prefixes]" \
|
||||
@ -28,8 +38,8 @@ __exa() {
|
||||
{-i,--inode}"[List each file's inode number]" \
|
||||
{-m,--modified}"[Use the modified timestamp field]" \
|
||||
{-S,--blocks}"[List each file's number of filesystem blocks]" \
|
||||
{-t,--time}"[Which time field to show]:(time field):(accessed created modified)" \
|
||||
--time-style"[How to format timestamps]:(time style):(default iso long-iso full-iso)" \
|
||||
{-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)" \
|
||||
{-u,--accessed}"[Use the accessed timestamp field]" \
|
||||
{-U,--created}"[Use the created timestamp field]" \
|
||||
--git"[List each file's Git status, if tracked]" \
|
||||
|
@ -1,5 +1,5 @@
|
||||
.hy
|
||||
.TH "exa" "1" "2017\-07\-07" "exa 0.7.0" ""
|
||||
.TH "exa" "1" "2019\-07\-15" "exa 0.9.0" ""
|
||||
.SH NAME
|
||||
.PP
|
||||
exa \- a modern replacement for ls
|
||||
@ -47,6 +47,11 @@ recurse into directories as a tree
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-F, \-\-classify
|
||||
display type indicator by file names
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-color, \-\-colour=\f[I]WHEN\f[]
|
||||
when to use terminal colours (always, automatic, never)
|
||||
.RS
|
||||
@ -56,6 +61,11 @@ when to use terminal colours (always, automatic, never)
|
||||
highlight levels of file sizes distinctly
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-icons
|
||||
display icons
|
||||
.RS
|
||||
.RE
|
||||
.SH FILTERING AND SORTING OPTIONS
|
||||
.TP
|
||||
.B \-a, \-\-all
|
||||
@ -69,6 +79,11 @@ list directories like regular files
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-L, \-\-level=\f[I]DEPTH\f[]
|
||||
limit the depth of recursion
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-r, \-\-reverse
|
||||
reverse the sort order
|
||||
.RS
|
||||
@ -76,8 +91,10 @@ reverse the sort order
|
||||
.TP
|
||||
.B \-s, \-\-sort=\f[I]SORT_FIELD\f[]
|
||||
which field to sort by.
|
||||
Valid fields are name, Name, extension, Extension, size, modified, accessed, created, inode, type, and none.
|
||||
Fields starting with a capital letter are case-sensitive.
|
||||
Valid fields are name, Name, extension, Extension, size, modified, changed, accessed, created, inode, type, and none.
|
||||
The modified field has the aliases date, time, and newest, and its reverse order has the aliases age and oldest.
|
||||
Fields starting with a capital letter will sort uppercase before lowercase: 'A' then 'B' then 'a' then 'b'.
|
||||
Fields starting with a lowercase letter will mix them: 'A' then 'a' then 'B' then 'b'.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
@ -86,10 +103,20 @@ Glob patterns, pipe-separated, of files to ignore
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-git\-ignore
|
||||
ignore files mentioned in '.gitignore'
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-group\-directories\-first
|
||||
list directories before other files
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-D, \-\-only\-dirs
|
||||
list only directories
|
||||
.RS
|
||||
.RE
|
||||
.SH LONG VIEW OPTIONS
|
||||
.PP
|
||||
These options are available when running with \f[C]\-\-long\f[]
|
||||
@ -125,11 +152,6 @@ list each file\[aq]s inode number
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-L, \-\-level=\f[I]DEPTH\f[]
|
||||
limit the depth of recursion
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-m, \-\-modified
|
||||
use the modified timestamp field
|
||||
.RS
|
||||
@ -141,7 +163,7 @@ list each file\[aq]s number of file system blocks
|
||||
.RE
|
||||
.TP
|
||||
.B \-t, \-\-time=\f[I]WORD\f[]
|
||||
which timestamp field to list (modified, accessed, created)
|
||||
which timestamp field to list (modified, changed, accessed, created)
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
@ -186,6 +208,246 @@ To display a tree of files, three levels deep:
|
||||
exa\ \-\-long\ \-\-tree\ \-\-level=3
|
||||
\f[]
|
||||
.fi
|
||||
.SH ENVIRONMENT VARIABLES
|
||||
.PP
|
||||
exa responds to the following environment variables:
|
||||
.SS \f[C]COLUMNS\f[]
|
||||
.PP
|
||||
Overrides the width of the terminal, in characters.
|
||||
For example, \f[C]COLUMNS=80\ exa\f[] will show a grid view with a
|
||||
maximum width of 80 characters.
|
||||
.PP
|
||||
This option won\[aq]t do anything when exa\[aq]s output doesn\[aq]t
|
||||
wrap, such as when using the \f[C]\-\-long\f[] view.
|
||||
.SS \f[C]EXA_STRICT\f[]
|
||||
.PP
|
||||
Enables \f[I]strict mode\f[], which will make exa 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
|
||||
\f[C]exa=exa\ \-\-sort=ext\f[] then running \f[C]exa\ \-\-sort=size\f[]
|
||||
with that alias will run \f[C]exa\ \-\-sort=ext\ \-\-sort=size\f[], 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.
|
||||
.PP
|
||||
This option is intended for use with automated scripts and other
|
||||
situations where you want to be \f[I]certain\f[] you\[aq]re typing in
|
||||
the right command.
|
||||
.SS \f[C]EXA_GRID_ROWS\f[]
|
||||
.PP
|
||||
Limits the grid\-details view (\f[C]exa\ \-\-grid\ \-\-long\f[]) so
|
||||
it\[aq]s only activated when at least the given number of rows of output
|
||||
would be generated.
|
||||
With widescreen displays, it\[aq]s 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
|
||||
it\[aq]s going to be worth using.
|
||||
.SS \f[C]LS_COLORS\f[] and \f[C]EXA_COLORS\f[]
|
||||
.PP
|
||||
The \f[C]EXA_COLORS\f[] variable is the traditional way of customising
|
||||
the colours used by \f[C]ls\f[].
|
||||
.PP
|
||||
You can use the \f[C]dircolors\f[] program to generate a script that
|
||||
sets the variable from an input file, or if you don\[aq]t mind editing
|
||||
long strings of text, you can just type it out directly.
|
||||
These variables have the following structure:
|
||||
.IP \[bu] 2
|
||||
A list of key\-value pairs separated by \f[C]=\f[], such as
|
||||
\f[C]*.txt=32\f[].
|
||||
.IP \[bu] 2
|
||||
Multiple ANSI formatting codes are separated by \f[C];\f[], such as
|
||||
\f[C]*.txt=32;1;4\f[].
|
||||
.IP \[bu] 2
|
||||
Finally, multiple pairs are separated by \f[C]:\f[], such as
|
||||
\f[C]*.txt=32:*.mp3=1;35\f[].
|
||||
.PP
|
||||
The key half of the pair can either be a two\-letter code or a file
|
||||
glob, and anything that\[aq]s not a valid code will be treated as a
|
||||
glob, including keys that happen to be two letters long.
|
||||
.PP
|
||||
\f[C]LS_COLORS\f[] can use these ten codes:
|
||||
.IP \[bu] 2
|
||||
\f[B]di\f[], directories
|
||||
.IP \[bu] 2
|
||||
\f[B]ex\f[], executable files
|
||||
.IP \[bu] 2
|
||||
\f[B]fi\f[], regular files
|
||||
.IP \[bu] 2
|
||||
\f[B]pi\f[], named pipes
|
||||
.IP \[bu] 2
|
||||
\f[B]so\f[], sockets
|
||||
.IP \[bu] 2
|
||||
\f[B]bd\f[], block devices
|
||||
.IP \[bu] 2
|
||||
\f[B]cd\f[], character devices
|
||||
.IP \[bu] 2
|
||||
\f[B]ln\f[], symlinks
|
||||
.IP \[bu] 2
|
||||
\f[B]or\f[], symlinks with no target
|
||||
.PP
|
||||
\f[C]EXA_COLORS\f[] can use many more:
|
||||
.IP \[bu] 2
|
||||
\f[B]ur\f[], the user\-read permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]uw\f[], the user\-write permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]ux\f[], the user\-execute permission bit for regular files
|
||||
.IP \[bu] 2
|
||||
\f[B]ue\f[], the user\-execute for other file kinds
|
||||
.IP \[bu] 2
|
||||
\f[B]gr\f[], the group\-read permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]gw\f[], the group\-write permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]gx\f[], the group\-execute permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]tr\f[], the others\-read permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]tw\f[], the others\-write permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]tx\f[], the others\-execute permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]su\f[], setuid, setgid, and sticky permission bits for files
|
||||
.IP \[bu] 2
|
||||
\f[B]sf\f[], setuid, setgid, and sticky for other file kinds
|
||||
.IP \[bu] 2
|
||||
\f[B]xa\f[], the extended attribute indicator
|
||||
.IP \[bu] 2
|
||||
\f[B]sn\f[], the numbers of a file\[aq]s size
|
||||
.IP \[bu] 2
|
||||
\f[B]sb\f[], the units of a file\[aq]s size
|
||||
.IP \[bu] 2
|
||||
\f[B]df\f[], a device\[aq]s major ID
|
||||
.IP \[bu] 2
|
||||
\f[B]ds\f[], a device\[aq]s minor ID
|
||||
.IP \[bu] 2
|
||||
\f[B]uu\f[], a user that\[aq]s you
|
||||
.IP \[bu] 2
|
||||
\f[B]un\f[], a user that\[aq]s someone else
|
||||
.IP \[bu] 2
|
||||
\f[B]gu\f[], a group that you belong to
|
||||
.IP \[bu] 2
|
||||
\f[B]gn\f[], a group you aren\[aq]t a member of
|
||||
.IP \[bu] 2
|
||||
\f[B]lc\f[], a number of hard links
|
||||
.IP \[bu] 2
|
||||
\f[B]lm\f[], a number of hard links for a regular file with at least two
|
||||
.IP \[bu] 2
|
||||
\f[B]ga\f[], a new flag in Git
|
||||
.IP \[bu] 2
|
||||
\f[B]gm\f[], a modified flag in Git
|
||||
.IP \[bu] 2
|
||||
\f[B]gd\f[], a deleted flag in Git
|
||||
.IP \[bu] 2
|
||||
\f[B]gv\f[], a renamed flag in Git
|
||||
.IP \[bu] 2
|
||||
\f[B]gt\f[], a modified metadata flag in Git
|
||||
.IP \[bu] 2
|
||||
\f[B]xx\f[], "punctuation", including many background UI elements
|
||||
.IP \[bu] 2
|
||||
\f[B]da\f[], a file\[aq]s date
|
||||
.IP \[bu] 2
|
||||
\f[B]in\f[], a file\[aq]s inode number
|
||||
.IP \[bu] 2
|
||||
\f[B]bl\f[], a file\[aq]s number of blocks
|
||||
.IP \[bu] 2
|
||||
\f[B]hd\f[], the header row of a table
|
||||
.IP \[bu] 2
|
||||
\f[B]lp\f[], the path of a symlink
|
||||
.IP \[bu] 2
|
||||
\f[B]cc\f[], an escaped character in a filename
|
||||
.IP \[bu] 2
|
||||
\f[B]bO\f[], the overlay style for broken symlink paths
|
||||
.PP
|
||||
Values in \f[C]EXA_COLORS\f[] override those given in
|
||||
\f[C]LS_COLORS\f[], so you don\[aq]t need to re\-write an existing
|
||||
\f[C]LS_COLORS\f[] variable with proprietary extensions.
|
||||
.PP
|
||||
Unlike some versions of \f[C]ls\f[], the given ANSI values must be valid
|
||||
colour codes: exa won\[aq]t just print out whichever characters are
|
||||
given.
|
||||
The codes accepted by exa are:
|
||||
.IP \[bu] 2
|
||||
\f[C]1\f[], for bold
|
||||
.IP \[bu] 2
|
||||
\f[C]4\f[], for underline
|
||||
.IP \[bu] 2
|
||||
\f[C]31\f[], for red text
|
||||
.IP \[bu] 2
|
||||
\f[C]32\f[], for green text
|
||||
.IP \[bu] 2
|
||||
\f[C]33\f[], for yellow text
|
||||
.IP \[bu] 2
|
||||
\f[C]34\f[], for blue text
|
||||
.IP \[bu] 2
|
||||
\f[C]35\f[], for purple text
|
||||
.IP \[bu] 2
|
||||
\f[C]36\f[], for cyan text
|
||||
.IP \[bu] 2
|
||||
\f[C]37\f[], for white text
|
||||
.IP \[bu] 2
|
||||
\f[C]38;5;\f[]\f[I]\f[C]nnn\f[]\f[], for a colour from 0 to 255 (replace
|
||||
the \f[I]nnn\f[] part)
|
||||
.PP
|
||||
Many terminals will treat bolded text as a different colour, or at least
|
||||
provide the option to.
|
||||
.PP
|
||||
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 \f[C]LS_COLORS="*.zip=32"\f[] will turn zip files
|
||||
green but leave the colours of other compressed files alone.
|
||||
.PP
|
||||
You can also disable this built\-in set entirely by including a
|
||||
\f[C]reset\f[] entry at the beginning of \f[C]EXA_COLORS\f[].
|
||||
So setting \f[C]EXA_COLORS="reset:*.txt=31"\f[] will highlight only text
|
||||
files; setting \f[C]EXA_COLORS="reset"\f[] will highlight nothing.
|
||||
.SS Examples
|
||||
.IP \[bu] 2
|
||||
Disable the "current user" highlighting: \f[C]EXA_COLORS="uu=0:gu=0"\f[]
|
||||
.IP \[bu] 2
|
||||
Turn the date column green: \f[C]EXA_COLORS="da=32"\f[]
|
||||
.IP \[bu] 2
|
||||
Highlight Vagrantfiles: \f[C]EXA_COLORS="Vagrantfile=1;4;33"\f[]
|
||||
.IP \[bu] 2
|
||||
Override the existing zip colour: \f[C]EXA_COLORS="*.zip=38;5;125"\f[]
|
||||
.IP \[bu] 2
|
||||
Markdown files a shade of green, log files a shade of grey:
|
||||
\f[C]EXA_COLORS="*.md=38;5;121:*.log=38;5;248"\f[]
|
||||
.SS BUILT\-IN EXTENSIONS
|
||||
.IP \[bu] 2
|
||||
"Immediate" files are the files you should look at when downloading and
|
||||
building a project for the first time: READMEs, Makefiles, Cargo.toml,
|
||||
and others.
|
||||
They\[aq]re highlighted in yellow and underlined.
|
||||
.IP \[bu] 2
|
||||
Images (png, jpeg, gif) are purple.
|
||||
.IP \[bu] 2
|
||||
Videos (mp4, ogv, m2ts) are a slightly purpler purple.
|
||||
.IP \[bu] 2
|
||||
Music (mp3, m4a, ogg) is a deeper purple.
|
||||
.IP \[bu] 2
|
||||
Lossless music (flac, alac, wav) is deeper than \f[I]that\f[] purple.
|
||||
In general, most media files are some shade of purple.
|
||||
.IP \[bu] 2
|
||||
Cryptographic files (asc, enc, p12) are a faint blue.
|
||||
.IP \[bu] 2
|
||||
Documents (pdf, doc, dvi) are a less faint blue.
|
||||
.IP \[bu] 2
|
||||
Compressed files (zip, tgz, Z) are red.
|
||||
.IP \[bu] 2
|
||||
Temporary files (tmp, swp, ~) are grey.
|
||||
.IP \[bu] 2
|
||||
Compiled files (class, o, pyc) are faint orange.
|
||||
A file is also counted as compiled if it uses a common extension and is
|
||||
in the same directory as one of its source files: \[aq]styles.css\[aq]
|
||||
will count as compiled when next to \[aq]styles.less\[aq] or
|
||||
\[aq]styles.sass\[aq], and \[aq]scripts.js\[aq] when next to
|
||||
\[aq]scripts.ts\[aq] or \[aq]scripts.coffee\[aq].
|
||||
.SH AUTHOR
|
||||
.PP
|
||||
\f[C]exa\f[] is maintained by Benjamin \[aq]ogham\[aq] Sago and many
|
||||
|
5
devtools/README.md
Normal file
5
devtools/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
## exa development tools
|
||||
|
||||
These scripts deal with things like packaging release-worthy versions of exa and making sure the published versions actually work.
|
||||
|
||||
They are **not general-purpose scripts** that you’re able to run from your main computer! They’re intended to be run from the Vagrant machines — they have commands such as ‘package-exa’ or ‘check-release’ that execute them instead.
|
56
devtools/dev-bash.sh
Normal file
56
devtools/dev-bash.sh
Normal file
@ -0,0 +1,56 @@
|
||||
# This file gets executed when a user starts a `bash` shell, usually because
|
||||
# they’ve just started a new Vagrant session with `vagrant ssh`. It configures
|
||||
# some (but not all) of the commands that you can use.
|
||||
|
||||
|
||||
# Display the installed versions of tools.
|
||||
# help banner
|
||||
bash /vagrant/devtools/dev-versions.sh
|
||||
|
||||
|
||||
# Configure the Cool Prompt™ (not actually trademarked).
|
||||
# The Cool Prompt tells you whether you’re in debug or strict mode, whether
|
||||
# you have colours configured, and whether your last command failed.
|
||||
function nonzero_return() { RETVAL=$?; [ $RETVAL -ne 0 ] && echo "$RETVAL "; }
|
||||
function debug_mode() { [ -n "$EXA_DEBUG" ] && echo "debug "; }
|
||||
function strict_mode() { [ -n "$EXA_STRICT" ] && echo "strict "; }
|
||||
function lsc_mode() { [ -n "$LS_COLORS" ] && echo "lsc "; }
|
||||
function exac_mode() { [ -n "$EXA_COLORS" ] && echo "exac "; }
|
||||
export PS1="\[\e[1;36m\]\h \[\e[32m\]\w \[\e[31m\]\`nonzero_return\`\[\e[35m\]\`debug_mode\`\[\e[32m\]\`lsc_mode\`\[\e[1;32m\]\`exac_mode\`\[\e[33m\]\`strict_mode\`\[\e[36m\]\\$\[\e[0m\] "
|
||||
|
||||
|
||||
# The ‘debug’ function lets you switch debug mode on and off.
|
||||
# Turn it on if you need to see exa’s debugging logs.
|
||||
function debug () {
|
||||
case "$1" in "on") export EXA_DEBUG=1 ;;
|
||||
"off") export EXA_DEBUG= ;;
|
||||
"") [ -n "$EXA_DEBUG" ] && echo "debug on" || echo "debug off" ;;
|
||||
*) echo "Usage: debug on|off"; return 1 ;; esac; }
|
||||
|
||||
# The ‘strict’ function lets you switch strict mode on and off.
|
||||
# Turn it on if you’d like exa’s command-line arguments checked.
|
||||
function strict () {
|
||||
case "$1" in "on") export EXA_STRICT=1 ;;
|
||||
"off") export EXA_STRICT= ;;
|
||||
"") [ -n "$EXA_STRICT" ] && echo "strict on" || echo "strict off" ;;
|
||||
*) echo "Usage: strict on|off"; return 1 ;; esac; }
|
||||
|
||||
# The ‘colors’ function sets or unsets the ‘LS_COLORS’ and ‘EXA_COLORS’
|
||||
# environment variables. There’s also a ‘hacker’ theme which turns everything
|
||||
# green, which is usually used for checking that all colour codes work, and
|
||||
# for looking cool while you phreak some mainframes or whatever.
|
||||
function colors () {
|
||||
case "$1" in
|
||||
"ls")
|
||||
export LS_COLORS="di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43"
|
||||
export EXA_COLORS="" ;;
|
||||
"hacker")
|
||||
export LS_COLORS="di=32:ex=32:fi=32:pi=32:so=32:bd=32:cd=32:ln=32:or=32:mi=32"
|
||||
export EXA_COLORS="ur=32:uw=32:ux=32:ue=32:gr=32:gw=32:gx=32:tr=32:tw=32:tx=32:su=32:sf=32:xa=32:sn=32:sb=32:df=32:ds=32:uu=32:un=32:gu=32:gn=32:lc=32:lm=32:ga=32:gm=32:gd=32:gv=32:gt=32:xx=32:da=32:in=32:bl=32:hd=32:lp=32:cc=32:" ;;
|
||||
"off")
|
||||
export LS_COLORS=
|
||||
export EXA_COLORS= ;;
|
||||
"")
|
||||
[ -n "$LS_COLORS" ] && echo "LS_COLORS=$LS_COLORS" || echo "ls-colors off"
|
||||
[ -n "$EXA_COLORS" ] && echo "EXA_COLORS=$EXA_COLORS" || echo "exa-colors off" ;;
|
||||
*) echo "Usage: ls-colors ls|hacker|off"; return 1 ;; esac; }
|
51
devtools/dev-download-and-check-release.sh
Normal file
51
devtools/dev-download-and-check-release.sh
Normal file
@ -0,0 +1,51 @@
|
||||
# This script downloads the published versions of exa from GitHub and my site,
|
||||
# checks that the checksums match, and makes sure the files at least unzip and
|
||||
# execute okay.
|
||||
#
|
||||
# The argument should be of the form “0.8.0”, no ‘v’. That version was the
|
||||
# first one to offer checksums, so it’s the minimum version that can be tested.
|
||||
|
||||
set +x
|
||||
trap 'exit' ERR
|
||||
|
||||
exa_version=$1
|
||||
if [[ -z "$exa_version" ]]; then
|
||||
echo "Please specify a version, such as '$0 0.8.0'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Delete anything that already exists
|
||||
rm -rfv "/tmp/${exa_version}-downloads"
|
||||
|
||||
|
||||
# Create a temporary directory and download exa into it
|
||||
mkdir "/tmp/${exa_version}-downloads"
|
||||
cd "/tmp/${exa_version}-downloads"
|
||||
|
||||
echo -e "\n\033[4mDownloading stuff...\033[0m"
|
||||
wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/exa-macos-x86_64-${exa_version}.zip"
|
||||
wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/exa-linux-x86_64-${exa_version}.zip"
|
||||
|
||||
wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/MD5SUMS"
|
||||
wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/SHA1SUMS"
|
||||
|
||||
|
||||
# Unzip the zips and check the sums
|
||||
echo -e "\n\033[4mExtracting that stuff...\033[0m"
|
||||
unzip "exa-macos-x86_64-${exa_version}.zip"
|
||||
unzip "exa-linux-x86_64-${exa_version}.zip"
|
||||
|
||||
echo -e "\n\033[4mValidating MD5 checksums...\033[0m"
|
||||
md5sum -c MD5SUMS
|
||||
|
||||
echo -e "\n\033[4mValidating SHA1 checksums...\033[0m"
|
||||
sha1sum -c SHA1SUMS
|
||||
|
||||
|
||||
# Finally, give the Linux version a go
|
||||
echo -e "\n\033[4mChecking it actually runs...\033[0m"
|
||||
./"exa-linux-x86_64" --version
|
||||
./"exa-linux-x86_64" --long
|
||||
|
||||
echo -e "\n\033[1;32mAll's lookin' good!\033[0m"
|
15
devtools/dev-generate-checksums.sh
Normal file
15
devtools/dev-generate-checksums.sh
Normal file
@ -0,0 +1,15 @@
|
||||
# This script generates the MD5SUMS and SHA1SUMS files.
|
||||
# You’ll need to have run ‘dev-download-and-check-release.sh’ and
|
||||
# ‘local-package-for-macos.sh’ scripts to generate the binaries first.
|
||||
|
||||
set +x
|
||||
trap 'exit' ERR
|
||||
|
||||
cd /vagrant
|
||||
rm -f MD5SUMS SHA1SUMS
|
||||
|
||||
echo -e "\n\033[4mValidating MD5 checksums...\033[0m"
|
||||
md5sum exa-linux-x86_64 exa-macos-x86_64 | tee MD5SUMS
|
||||
|
||||
echo -e "\n\033[4mValidating SHA1 checksums...\033[0m"
|
||||
sha1sum exa-linux-x86_64 exa-macos-x86_64 | tee SHA1SUMS
|
12
devtools/dev-help-testvm.sh
Normal file
12
devtools/dev-help-testvm.sh
Normal file
@ -0,0 +1,12 @@
|
||||
# This file is like the other one, except for the testing VM.
|
||||
# It also gets dumped into /etc/motd.
|
||||
|
||||
|
||||
echo -e "
|
||||
\033[1;33mThe exa testing environment!\033[0m
|
||||
This machine is dependency-free, and can be used to test that
|
||||
released versions of exa still work on vanilla Linux installs.
|
||||
|
||||
\033[4mCommands\033[0m
|
||||
\033[32;1mcheck-release\033[0m to download and verify released binaries
|
||||
"
|
21
devtools/dev-help.sh
Normal file
21
devtools/dev-help.sh
Normal file
@ -0,0 +1,21 @@
|
||||
# This file prints out some help text that says which commands are available
|
||||
# in the VM. It gets executed during Vagrant provisioning and its output gets
|
||||
# dumped into /etc/motd, to print it when a user starts a new Vagrant session.
|
||||
|
||||
|
||||
echo -e "
|
||||
\033[1;33mThe exa development environment!\033[0m
|
||||
exa's source is available at \033[33m/vagrant\033[0m.
|
||||
Binaries get built into \033[33m/home/vagrant/target\033[0m.
|
||||
|
||||
\033[4mCommands\033[0m
|
||||
\033[32;1mexa\033[0m to run the built version of exa
|
||||
\033[32;1mbuild-exa\033[0m (or \033[32;1mb\033[0m) to run \033[1mcargo build\033[0m
|
||||
\033[32;1mtest-exa\033[0m (or \033[32;1mt\033[0m) to run \033[1mcargo test\033[0m
|
||||
\033[32;1mrun-xtests\033[0m (or \033[32;1mx\033[0m) to run the extended tests
|
||||
\033[32;1mcompile-exa\033[0m (or \033[32;1mc\033[0m) to run the above three
|
||||
\033[32;1mdebug\033[0m to toggle printing logs
|
||||
\033[32;1mstrict\033[0m to toggle strict mode
|
||||
\033[32;1mcolors\033[0m to toggle custom colours
|
||||
\033[32;1mhalp\033[0m to show all this again
|
||||
"
|
75
devtools/dev-package-for-linux.sh
Normal file
75
devtools/dev-package-for-linux.sh
Normal file
@ -0,0 +1,75 @@
|
||||
set -e
|
||||
|
||||
# This script builds a publishable release-worthy version of exa.
|
||||
# It gets the version number, builds exa using cargo, tests it, strips the
|
||||
# binary, compresses it into a zip, then puts it in /vagrant so it’s
|
||||
# accessible from the host machine.
|
||||
#
|
||||
# If you’re in the VM, you can run it using the ‘package-exa’ command.
|
||||
|
||||
|
||||
# Linux check!
|
||||
uname=`uname -s`
|
||||
if [[ "$uname" != "Linux" ]]; then
|
||||
echo "Gotta be on Linux to run this (detected '$uname')!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# First, we need to get the version number to figure out what to call the zip.
|
||||
# We do this by getting the first line from the Cargo.toml that matches
|
||||
# /version/, removing its whitespace, and building a command out of it, so the
|
||||
# shell executes something like `exa_version="0.8.0"`, which it understands as
|
||||
# a variable definition. Hey, it’s not a hack if it works.
|
||||
toml_file="/vagrant/Cargo.toml"
|
||||
eval exa_$(grep version $toml_file | head -n 1 | sed "s/ //g")
|
||||
if [ -z "$exa_version" ]; then
|
||||
echo "Failed to parse version number! Can't build exa!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Weekly builds have a bit more information in their version number (see build.rs).
|
||||
if [[ "$1" == "--weekly" ]]; then
|
||||
git_hash=`GIT_DIR=/vagrant/.git git rev-parse --short --verify HEAD`
|
||||
date=`date +"%Y-%m-%d"`
|
||||
echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash"
|
||||
else
|
||||
echo "Building exa v$exa_version"
|
||||
fi
|
||||
|
||||
# Compilation is done in --release mode, which takes longer but produces a
|
||||
# faster binary. This binary gets built to a different place, so the extended
|
||||
# tests script needs to be told which one to use.
|
||||
echo -e "\n\033[4mCompiling release version of exa...\033[0m"
|
||||
exa_linux_binary="/vagrant/exa-linux-x86_64"
|
||||
rm -vf "$exa_linux_binary"
|
||||
cargo build --release --manifest-path "$toml_file"
|
||||
cargo test --release --manifest-path "$toml_file" --lib -- --quiet
|
||||
/vagrant/xtests/run.sh --release
|
||||
cp /home/vagrant/target/release/exa "$exa_linux_binary"
|
||||
|
||||
# Stripping the binary before distributing it removes a bunch of debugging
|
||||
# symbols, saving some space.
|
||||
echo -e "\n\033[4mStripping binary...\033[0m"
|
||||
strip -v "$exa_linux_binary"
|
||||
|
||||
# Compress the binary for upload. The ‘-j’ flag is necessary to avoid the
|
||||
# /vagrant path being in the zip too. Only the zip gets the version number, so
|
||||
# the binaries can have consistent names, and it’s still possible to tell
|
||||
# different *downloads* apart.
|
||||
echo -e "\n\033[4mZipping binary...\033[0m"
|
||||
if [[ "$1" == "--weekly" ]]
|
||||
then exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}-${date}-${git_hash}.zip"
|
||||
else exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}.zip"
|
||||
fi
|
||||
rm -vf "$exa_linux_zip"
|
||||
zip -j "$exa_linux_zip" "$exa_linux_binary"
|
||||
|
||||
# There was a problem a while back where a library was getting unknowingly
|
||||
# *dynamically* linked, which broke the whole ‘self-contained binary’ concept.
|
||||
# So dump the linker table, in case anything unscrupulous shows up.
|
||||
echo -e "\n\033[4mLibraries linked:\033[0m"
|
||||
ldd "$exa_linux_binary" | sed "s/\t//"
|
||||
|
||||
# Might as well use it to test itself, right?
|
||||
echo -e "\n\033[4mAll done! Files produced:\033[0m"
|
||||
"$exa_linux_binary" "$exa_linux_binary" "$exa_linux_zip" -lB
|
7
devtools/dev-run-debug.sh
Executable file
7
devtools/dev-run-debug.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
if [[ -f ~/target/debug/exa ]]; then
|
||||
~/target/debug/exa "$@"
|
||||
else
|
||||
echo -e "Debug exa binary does not exist!"
|
||||
echo -e "Run \033[32;1mb\033[0m or \033[32;1mbuild-exa\033[0m to create it"
|
||||
fi
|
7
devtools/dev-run-release.sh
Executable file
7
devtools/dev-run-release.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
if [[ -f ~/target/release/exa ]]; then
|
||||
~/target/release/exa "$@"
|
||||
else
|
||||
echo -e "Release exa binary does not exist!"
|
||||
echo -e "Run \033[32;1mb --release\033[0m or \033[32;1mbuild-exa --release\033[0m to create it"
|
||||
fi
|
8
devtools/dev-versions.sh
Normal file
8
devtools/dev-versions.sh
Normal file
@ -0,0 +1,8 @@
|
||||
# Displays the installed versions of Rust and Cargo.
|
||||
# This gets run from ‘dev-bash.sh’, which gets run from ‘~/.bash_profile’, so
|
||||
# the versions gets displayed after the help text for a new Vagrant session.
|
||||
|
||||
echo -e "\\033[4mVersions\\033[0m"
|
||||
rustc --version
|
||||
cargo --version
|
||||
echo
|
83
devtools/local-package-for-macos.sh
Normal file
83
devtools/local-package-for-macos.sh
Normal file
@ -0,0 +1,83 @@
|
||||
set -e
|
||||
|
||||
# This script builds a publishable release-worthy version of exa.
|
||||
# It gets the version number, builds exa using cargo, tests it, strips the
|
||||
# binary, and compresses it into a zip.
|
||||
#
|
||||
# It’s *mostly* the same as dev-package-for-linux.sh, except with some
|
||||
# Mach-specific things (otool instead of ldd), BSD-coreutils-specific things,
|
||||
# and it doesn’t run the xtests.
|
||||
|
||||
|
||||
# Virtualising macOS is a legal minefield, so this script is ‘local’ instead
|
||||
# of ‘dev’: I run it from my actual machine, rather than from a VM.
|
||||
uname=`uname -s`
|
||||
if [[ "$uname" != "Darwin" ]]; then
|
||||
echo "Gotta be on Darwin to run this (detected '$uname')!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# First, we need to get the version number to figure out what to call the zip.
|
||||
# We do this by getting the first line from the Cargo.toml that matches
|
||||
# /version/, removing its whitespace, and building a command out of it, so the
|
||||
# shell executes something like `exa_version="0.8.0"`, which it understands as
|
||||
# a variable definition. Hey, it’s not a hack if it works.
|
||||
#
|
||||
# Because this can’t use the absolute /vagrant path, this has to use what this
|
||||
# SO answer calls a “quoting disaster”: https://stackoverflow.com/a/20196098/3484614
|
||||
# You will also need GNU coreutils: https://stackoverflow.com/a/4031502/3484614
|
||||
exa_root="$(dirname "$(dirname "$(greadlink -fm "$0")")")"
|
||||
toml_file="$exa_root"/Cargo.toml
|
||||
eval exa_$(grep version $toml_file | head -n 1 | sed "s/ //g")
|
||||
if [ -z "$exa_version" ]; then
|
||||
echo "Failed to parse version number! Can't build exa!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Weekly builds have a bit more information in their version number (see build.rs).
|
||||
if [[ "$1" == "--weekly" ]]; then
|
||||
git_hash=`GIT_DIR=$exa_root/.git git rev-parse --short --verify HEAD`
|
||||
date=`date +"%Y-%m-%d"`
|
||||
echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash"
|
||||
else
|
||||
echo "Building exa v$exa_version"
|
||||
fi
|
||||
|
||||
# Compilation is done in --release mode, which takes longer but produces a
|
||||
# faster binary.
|
||||
echo -e "\n\033[4mCompiling release version of exa...\033[0m"
|
||||
exa_macos_binary="$exa_root/exa-macos-x86_64"
|
||||
rm -vf "$exa_macos_binary" | sed 's/^/removing /'
|
||||
cargo build --release --manifest-path "$toml_file"
|
||||
cargo test --release --manifest-path "$toml_file" --lib -- --quiet
|
||||
# we can’t run the xtests outside the VM!
|
||||
#/vagrant/xtests/run.sh --release
|
||||
cp "$exa_root"/target/release/exa "$exa_macos_binary"
|
||||
|
||||
# Stripping the binary before distributing it removes a bunch of debugging
|
||||
# symbols, saving some space.
|
||||
echo -e "\n\033[4mStripping binary...\033[0m"
|
||||
strip "$exa_macos_binary"
|
||||
echo "strip $exa_macos_binary"
|
||||
|
||||
# Compress the binary for upload. The ‘-j’ flag is necessary to avoid the
|
||||
# current path being in the zip too. Only the zip gets the version number, so
|
||||
# the binaries can have consistent names, and it’s still possible to tell
|
||||
# different *downloads* apart.
|
||||
echo -e "\n\033[4mZipping binary...\033[0m"
|
||||
if [[ "$1" == "--weekly" ]]
|
||||
then exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}-${date}-${git_hash}.zip"
|
||||
else exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}.zip"
|
||||
fi
|
||||
rm -vf "$exa_macos_zip" | sed 's/^/removing /'
|
||||
zip -j "$exa_macos_zip" "$exa_macos_binary"
|
||||
|
||||
# There was a problem a while back where a library was getting unknowingly
|
||||
# *dynamically* linked, which broke the whole ‘self-contained binary’ concept.
|
||||
# So dump the linker table, in case anything unscrupulous shows up.
|
||||
echo -e "\n\033[4mLibraries linked:\033[0m"
|
||||
otool -L "$exa_macos_binary" | sed 's/^[[:space:]]*//'
|
||||
|
||||
# Might as well use it to test itself, right?
|
||||
echo -e "\n\033[4mAll done! Files produced:\033[0m"
|
||||
"$exa_macos_binary" "$exa_macos_binary" "$exa_macos_zip" -lB
|
1
snap/.gitignore
vendored
Normal file
1
snap/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.snapcraft
|
25
snap/snapcraft.yaml
Normal file
25
snap/snapcraft.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
name: exa
|
||||
version: 'latest'
|
||||
summary: Replacement for 'ls' written in Rust
|
||||
description: |
|
||||
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. exa is written in Rust, so it’s small, fast, and portable.
|
||||
|
||||
grade: stable
|
||||
confinement: classic
|
||||
|
||||
apps:
|
||||
exa:
|
||||
command: exa
|
||||
|
||||
parts:
|
||||
exa:
|
||||
plugin: rust
|
||||
source: .
|
||||
stage-packages:
|
||||
- libgit2-24
|
||||
- cmake
|
||||
- libz-dev
|
@ -1,14 +1,17 @@
|
||||
extern crate exa;
|
||||
use exa::Exa;
|
||||
|
||||
use std::env::args_os;
|
||||
use std::ffi::OsString;
|
||||
use std::env::{args_os, var_os};
|
||||
use std::io::{stdout, stderr, Write, ErrorKind};
|
||||
use std::process::exit;
|
||||
|
||||
|
||||
fn main() {
|
||||
let args = args_os().skip(1);
|
||||
match Exa::new(args, &mut stdout()) {
|
||||
configure_logger();
|
||||
|
||||
let args: Vec<OsString> = args_os().skip(1).collect();
|
||||
match Exa::from_args(args.iter(), &mut stdout()) {
|
||||
Ok(mut exa) => {
|
||||
match exa.run() {
|
||||
Ok(exit_status) => exit(exit_status),
|
||||
@ -16,7 +19,7 @@ fn main() {
|
||||
match e.kind() {
|
||||
ErrorKind::BrokenPipe => exit(exits::SUCCESS),
|
||||
_ => {
|
||||
writeln!(stderr(), "{}", e).unwrap();
|
||||
eprintln!("{}", e);
|
||||
exit(exits::RUNTIME_ERROR);
|
||||
},
|
||||
};
|
||||
@ -25,18 +28,52 @@ fn main() {
|
||||
},
|
||||
|
||||
Err(ref e) if e.is_error() => {
|
||||
writeln!(stderr(), "{}", e).unwrap();
|
||||
let mut stderr = stderr();
|
||||
writeln!(stderr, "{}", e).unwrap();
|
||||
|
||||
if let Some(s) = e.suggestion() {
|
||||
let _ = writeln!(stderr, "{}", s);
|
||||
}
|
||||
|
||||
exit(exits::OPTIONS_ERROR);
|
||||
},
|
||||
|
||||
Err(ref e) => {
|
||||
writeln!(stdout(), "{}", e).unwrap();
|
||||
println!("{}", e);
|
||||
exit(exits::SUCCESS);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// Sets up a global logger if one is asked for.
|
||||
/// The ‘EXA_DEBUG’ environment variable controls whether log messages are
|
||||
/// displayed or not. Currently there are just two settings (on and off).
|
||||
///
|
||||
/// This can’t be done in exa’s own option parsing because that part of it
|
||||
/// logs as well, so by the time execution gets there, the logger needs to
|
||||
/// have already been set up.
|
||||
pub fn configure_logger() {
|
||||
extern crate env_logger;
|
||||
extern crate log;
|
||||
|
||||
let present = match var_os(exa::vars::EXA_DEBUG) {
|
||||
Some(debug) => debug.len() > 0,
|
||||
None => false,
|
||||
};
|
||||
|
||||
let mut logs = env_logger::Builder::new();
|
||||
if present {
|
||||
logs.filter(None, log::LevelFilter::Debug);
|
||||
}
|
||||
else {
|
||||
logs.filter(None, log::LevelFilter::Off);
|
||||
}
|
||||
|
||||
logs.init()
|
||||
}
|
||||
|
||||
|
||||
extern crate libc;
|
||||
#[allow(trivial_numeric_casts)]
|
||||
mod exits {
|
||||
|
139
src/exa.rs
139
src/exa.rs
@ -3,7 +3,6 @@
|
||||
|
||||
extern crate ansi_term;
|
||||
extern crate datetime;
|
||||
extern crate getopts;
|
||||
extern crate glob;
|
||||
extern crate libc;
|
||||
extern crate locale;
|
||||
@ -15,33 +14,38 @@ extern crate term_grid;
|
||||
extern crate unicode_width;
|
||||
extern crate users;
|
||||
extern crate zoneinfo_compiled;
|
||||
extern crate term_size;
|
||||
|
||||
#[cfg(feature="git")] extern crate git2;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
#[macro_use] extern crate log;
|
||||
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::env::var_os;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::io::{stderr, Write, Result as IOResult};
|
||||
use std::path::{Component, PathBuf};
|
||||
|
||||
use ansi_term::{ANSIStrings, Style};
|
||||
|
||||
use fs::{Dir, File};
|
||||
use options::{Options, View, Mode};
|
||||
use fs::feature::ignore::IgnoreCache;
|
||||
use fs::feature::git::GitCache;
|
||||
use options::{Options, Vars};
|
||||
pub use options::vars;
|
||||
pub use options::Misfire;
|
||||
use output::{escape, lines, grid, grid_details, details};
|
||||
use output::{escape, lines, grid, grid_details, details, View, Mode};
|
||||
|
||||
mod fs;
|
||||
mod info;
|
||||
mod options;
|
||||
mod output;
|
||||
mod term;
|
||||
mod style;
|
||||
|
||||
|
||||
/// The main program wrapper.
|
||||
pub struct Exa<'w, W: Write + 'w> {
|
||||
pub struct Exa<'args, 'w, W: Write + 'w> {
|
||||
|
||||
/// List of command-line options, having been successfully parsed.
|
||||
pub options: Options,
|
||||
@ -53,14 +57,65 @@ pub struct Exa<'w, W: Write + 'w> {
|
||||
|
||||
/// List of the free command-line arguments that should correspond to file
|
||||
/// names (anything that isn’t an option).
|
||||
pub args: Vec<String>,
|
||||
pub args: Vec<&'args OsStr>,
|
||||
|
||||
/// A global Git cache, if the option was passed in.
|
||||
/// This has to last the lifetime of the program, because the user might
|
||||
/// want to list several directories in the same repository.
|
||||
pub git: Option<GitCache>,
|
||||
|
||||
/// A cache of git-ignored files.
|
||||
/// This lasts the lifetime of the program too, for the same reason.
|
||||
pub ignore: Option<IgnoreCache>,
|
||||
}
|
||||
|
||||
impl<'w, W: Write + 'w> Exa<'w, W> {
|
||||
pub fn new<C>(args: C, writer: &'w mut W) -> Result<Exa<'w, W>, Misfire>
|
||||
where C: IntoIterator, C::Item: AsRef<OsStr> {
|
||||
Options::getopts(args).map(move |(options, args)| {
|
||||
Exa { options, writer, args }
|
||||
/// The “real” environment variables type.
|
||||
/// Instead of just calling `var_os` from within the options module,
|
||||
/// the method of looking up environment variables has to be passed in.
|
||||
struct LiveVars;
|
||||
impl Vars for LiveVars {
|
||||
fn get(&self, name: &'static str) -> Option<OsString> {
|
||||
var_os(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Git cache populated with the arguments that are going to be
|
||||
/// listed before they’re actually listed, if the options demand it.
|
||||
fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
|
||||
if options.should_scan_for_git() {
|
||||
Some(args.iter().map(PathBuf::from).collect())
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn ignore_cache(options: &Options) -> Option<IgnoreCache> {
|
||||
use fs::filter::GitIgnore;
|
||||
|
||||
match options.filter.git_ignore {
|
||||
GitIgnore::CheckAndIgnore => Some(IgnoreCache::new()),
|
||||
GitIgnore::Off => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
|
||||
pub fn from_args<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire>
|
||||
where I: Iterator<Item=&'args OsString> {
|
||||
Options::parse(args, &LiveVars).map(move |(options, mut args)| {
|
||||
debug!("Dir action from arguments: {:#?}", options.dir_action);
|
||||
debug!("Filter from arguments: {:#?}", options.filter);
|
||||
debug!("View from arguments: {:#?}", options.view.mode);
|
||||
|
||||
// List the current directory by default, like ls.
|
||||
// This has to be done here, otherwise git_options won’t see it.
|
||||
if args.is_empty() {
|
||||
args = vec![ OsStr::new(".") ];
|
||||
}
|
||||
|
||||
let git = git_options(&options, &args);
|
||||
let ignore = ignore_cache(&options);
|
||||
Exa { options, writer, args, git, ignore }
|
||||
})
|
||||
}
|
||||
|
||||
@ -69,22 +124,17 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
|
||||
let mut dirs = Vec::new();
|
||||
let mut exit_status = 0;
|
||||
|
||||
// List the current directory by default, like ls.
|
||||
if self.args.is_empty() {
|
||||
self.args.push(".".to_owned());
|
||||
}
|
||||
|
||||
for file_name in &self.args {
|
||||
match File::new(PathBuf::from(file_name), None, None) {
|
||||
for file_path in &self.args {
|
||||
match File::from_args(PathBuf::from(file_path), None, None) {
|
||||
Err(e) => {
|
||||
exit_status = 2;
|
||||
writeln!(stderr(), "{}: {}", file_name, e)?;
|
||||
writeln!(stderr(), "{:?}: {}", file_path, e)?;
|
||||
},
|
||||
Ok(f) => {
|
||||
if f.is_directory() && !self.options.dir_action.treat_dirs_as_files() {
|
||||
match f.to_dir(self.options.should_scan_for_git()) {
|
||||
if f.points_to_directory() && !self.options.dir_action.treat_dirs_as_files() {
|
||||
match f.to_dir() {
|
||||
Ok(d) => dirs.push(d),
|
||||
Err(e) => writeln!(stderr(), "{}: {}", file_name, e)?,
|
||||
Err(e) => writeln!(stderr(), "{:?}: {}", file_path, e)?,
|
||||
}
|
||||
}
|
||||
else {
|
||||
@ -116,7 +166,7 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
|
||||
first = false;
|
||||
}
|
||||
else {
|
||||
write!(self.writer, "\n")?;
|
||||
writeln!(self.writer)?;
|
||||
}
|
||||
|
||||
if !is_only_dir {
|
||||
@ -126,7 +176,7 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
|
||||
}
|
||||
|
||||
let mut children = Vec::new();
|
||||
for file in dir.files(self.options.filter.dot_filter) {
|
||||
for file in dir.files(self.options.filter.dot_filter, self.ignore.as_ref()) {
|
||||
match file {
|
||||
Ok(file) => children.push(file),
|
||||
Err((path, e)) => writeln!(stderr(), "[{}: {}]", path.display(), e)?,
|
||||
@ -141,8 +191,8 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
|
||||
if !recurse_opts.tree && !recurse_opts.is_too_deep(depth) {
|
||||
|
||||
let mut child_dirs = Vec::new();
|
||||
for child_dir in children.iter().filter(|f| f.is_directory()) {
|
||||
match child_dir.to_dir(false) {
|
||||
for child_dir in children.iter().filter(|f| f.is_directory() && !f.is_all_all) {
|
||||
match child_dir.to_dir() {
|
||||
Ok(d) => child_dirs.push(d),
|
||||
Err(e) => writeln!(stderr(), "{}: {}", child_dir.path.display(), e)?,
|
||||
}
|
||||
@ -171,10 +221,33 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
|
||||
let View { ref mode, ref colours, ref style } = self.options.view;
|
||||
|
||||
match *mode {
|
||||
Mode::Lines => lines::Render { files, colours, style }.render(self.writer),
|
||||
Mode::Grid(ref opts) => grid::Render { files, colours, style, opts }.render(self.writer),
|
||||
Mode::Details(ref opts) => details::Render { dir, files, colours, style, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(self.writer),
|
||||
Mode::GridDetails(ref grid, ref details) => grid_details::Render { dir, files, colours, style, grid, details, filter: &self.options.filter }.render(self.writer),
|
||||
Mode::Lines(ref opts) => {
|
||||
let r = lines::Render { files, colours, style, opts };
|
||||
r.render(self.writer)
|
||||
}
|
||||
|
||||
Mode::Grid(ref opts) => {
|
||||
let r = grid::Render { files, colours, style, opts };
|
||||
r.render(self.writer)
|
||||
}
|
||||
|
||||
Mode::Details(ref opts) => {
|
||||
let filter = &self.options.filter;
|
||||
let recurse = self.options.dir_action.recurse_options();
|
||||
|
||||
let r = details::Render { dir, files, colours, style, opts, filter, recurse };
|
||||
r.render(self.git.as_ref(), self.ignore.as_ref(), self.writer)
|
||||
}
|
||||
|
||||
Mode::GridDetails(ref opts) => {
|
||||
let grid = &opts.grid;
|
||||
let filter = &self.options.filter;
|
||||
let details = &opts.details;
|
||||
let row_threshold = opts.row_threshold;
|
||||
|
||||
let r = grid_details::Render { dir, files, colours, style, grid, details, filter, row_threshold };
|
||||
r.render(self.git.as_ref(), self.writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
106
src/fs/dir.rs
106
src/fs/dir.rs
@ -3,8 +3,8 @@ use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::slice::Iter as SliceIter;
|
||||
|
||||
use fs::feature::Git;
|
||||
use fs::{File, fields};
|
||||
use fs::File;
|
||||
use fs::feature::ignore::IgnoreCache;
|
||||
|
||||
|
||||
/// A **Dir** provides a cached list of the file paths in a directory that's
|
||||
@ -20,10 +20,6 @@ pub struct Dir {
|
||||
|
||||
/// The path that was read.
|
||||
pub path: PathBuf,
|
||||
|
||||
/// Holds a `Git` object if scanning for Git repositories is switched on,
|
||||
/// and this directory happens to contain one.
|
||||
git: Option<Git>,
|
||||
}
|
||||
|
||||
impl Dir {
|
||||
@ -36,23 +32,27 @@ impl Dir {
|
||||
/// The `read_dir` iterator doesn’t actually yield the `.` and `..`
|
||||
/// entries, so if the user wants to see them, we’ll have to add them
|
||||
/// ourselves after the files have been read.
|
||||
pub fn read_dir(path: PathBuf, git: bool) -> IOResult<Dir> {
|
||||
let contents: Vec<PathBuf> = try!(fs::read_dir(&path)?
|
||||
.map(|result| result.map(|entry| entry.path()))
|
||||
.collect());
|
||||
pub fn read_dir(path: PathBuf) -> IOResult<Dir> {
|
||||
info!("Reading directory {:?}", &path);
|
||||
|
||||
let git = if git { Git::scan(&path).ok() } else { None };
|
||||
Ok(Dir { contents, path, git })
|
||||
let contents = fs::read_dir(&path)?
|
||||
.map(|result| result.map(|entry| entry.path()))
|
||||
.collect::<Result<_,_>>()?;
|
||||
|
||||
Ok(Dir { contents, path })
|
||||
}
|
||||
|
||||
/// Produce an iterator of IO results of trying to read all the files in
|
||||
/// this directory.
|
||||
pub fn files(&self, dots: DotFilter) -> Files {
|
||||
pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, ignore: Option<&'ig IgnoreCache>) -> Files<'dir, 'ig> {
|
||||
if let Some(i) = ignore { i.discover_underneath(&self.path); }
|
||||
|
||||
Files {
|
||||
inner: self.contents.iter(),
|
||||
dir: self,
|
||||
dotfiles: dots.shows_dotfiles(),
|
||||
dots: dots.dots(),
|
||||
ignore,
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,25 +65,11 @@ impl Dir {
|
||||
pub fn join(&self, child: &Path) -> PathBuf {
|
||||
self.path.join(child)
|
||||
}
|
||||
|
||||
/// Return whether there's a Git repository on or above this directory.
|
||||
pub fn has_git_repo(&self) -> bool {
|
||||
self.git.is_some()
|
||||
}
|
||||
|
||||
/// Get a string describing the Git status of the given file.
|
||||
pub fn git_status(&self, path: &Path, prefix_lookup: bool) -> fields::Git {
|
||||
match (&self.git, prefix_lookup) {
|
||||
(&Some(ref git), false) => git.status(path),
|
||||
(&Some(ref git), true) => git.dir_status(path),
|
||||
(&None, _) => fields::Git::empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Iterator over reading the contents of a directory as `File` objects.
|
||||
pub struct Files<'dir> {
|
||||
pub struct Files<'dir, 'ig> {
|
||||
|
||||
/// The internal iterator over the paths that have been read already.
|
||||
inner: SliceIter<'dir, PathBuf>,
|
||||
@ -96,10 +82,12 @@ pub struct Files<'dir> {
|
||||
|
||||
/// Whether the `.` or `..` directories should be produced first, before
|
||||
/// any files have been listed.
|
||||
dots: Dots,
|
||||
dots: DotsNext,
|
||||
|
||||
ignore: Option<&'ig IgnoreCache>,
|
||||
}
|
||||
|
||||
impl<'dir> Files<'dir> {
|
||||
impl<'dir, 'ig> Files<'dir, 'ig> {
|
||||
fn parent(&self) -> PathBuf {
|
||||
// We can’t use `Path#parent` here because all it does is remove the
|
||||
// last path component, which is no good for us if the path is
|
||||
@ -115,9 +103,13 @@ impl<'dir> Files<'dir> {
|
||||
loop {
|
||||
if let Some(path) = self.inner.next() {
|
||||
let filename = File::filename(path);
|
||||
if !self.dotfiles && filename.starts_with(".") { continue }
|
||||
if !self.dotfiles && filename.starts_with('.') { continue }
|
||||
|
||||
return Some(File::new(path.clone(), self.dir, filename)
|
||||
if let Some(i) = self.ignore {
|
||||
if i.is_ignored(path) { continue }
|
||||
}
|
||||
|
||||
return Some(File::from_args(path.clone(), self.dir, filename)
|
||||
.map_err(|e| (path.clone(), e)))
|
||||
}
|
||||
else {
|
||||
@ -129,35 +121,37 @@ impl<'dir> Files<'dir> {
|
||||
|
||||
/// The dot directories that need to be listed before actual files, if any.
|
||||
/// If these aren’t being printed, then `FilesNext` is used to skip them.
|
||||
enum Dots {
|
||||
enum DotsNext {
|
||||
|
||||
/// List the `.` directory next.
|
||||
DotNext,
|
||||
Dot,
|
||||
|
||||
/// List the `..` directory next.
|
||||
DotDotNext,
|
||||
DotDot,
|
||||
|
||||
/// Forget about the dot directories and just list files.
|
||||
FilesNext,
|
||||
Files,
|
||||
}
|
||||
|
||||
|
||||
impl<'dir> Iterator for Files<'dir> {
|
||||
impl<'dir, 'ig> Iterator for Files<'dir, 'ig> {
|
||||
type Item = Result<File<'dir>, (PathBuf, io::Error)>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Dots::DotNext = self.dots {
|
||||
self.dots = Dots::DotDotNext;
|
||||
Some(File::new(self.dir.path.to_path_buf(), self.dir, String::from("."))
|
||||
.map_err(|e| (Path::new(".").to_path_buf(), e)))
|
||||
}
|
||||
else if let Dots::DotDotNext = self.dots {
|
||||
self.dots = Dots::FilesNext;
|
||||
Some(File::new(self.parent(), self.dir, String::from(".."))
|
||||
.map_err(|e| (self.parent(), e)))
|
||||
}
|
||||
else {
|
||||
self.next_visible_file()
|
||||
match self.dots {
|
||||
DotsNext::Dot => {
|
||||
self.dots = DotsNext::DotDot;
|
||||
Some(File::new_aa_current(self.dir)
|
||||
.map_err(|e| (Path::new(".").to_path_buf(), e)))
|
||||
},
|
||||
DotsNext::DotDot => {
|
||||
self.dots = DotsNext::Files;
|
||||
Some(File::new_aa_parent(self.parent(), self.dir)
|
||||
.map_err(|e| (self.parent(), e)))
|
||||
},
|
||||
DotsNext::Files => {
|
||||
self.next_visible_file()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -188,8 +182,8 @@ impl Default for DotFilter {
|
||||
impl DotFilter {
|
||||
|
||||
/// Whether this filter should show dotfiles in a listing.
|
||||
fn shows_dotfiles(&self) -> bool {
|
||||
match *self {
|
||||
fn shows_dotfiles(self) -> bool {
|
||||
match self {
|
||||
DotFilter::JustFiles => false,
|
||||
DotFilter::Dotfiles => true,
|
||||
DotFilter::DotfilesAndDots => true,
|
||||
@ -197,11 +191,11 @@ impl DotFilter {
|
||||
}
|
||||
|
||||
/// Whether this filter should add dot directories to a listing.
|
||||
fn dots(&self) -> Dots {
|
||||
match *self {
|
||||
DotFilter::JustFiles => Dots::FilesNext,
|
||||
DotFilter::Dotfiles => Dots::FilesNext,
|
||||
DotFilter::DotfilesAndDots => Dots::DotNext,
|
||||
fn dots(self) -> DotsNext {
|
||||
match self {
|
||||
DotFilter::JustFiles => DotsNext::Files,
|
||||
DotFilter::Dotfiles => DotsNext::Files,
|
||||
DotFilter::DotfilesAndDots => DotsNext::Dot,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
82
src/fs/dir_action.rs
Normal file
82
src/fs/dir_action.rs
Normal file
@ -0,0 +1,82 @@
|
||||
//! What to do when encountering a directory?
|
||||
|
||||
/// The action to take when trying to list a file that turns out to be a
|
||||
/// directory.
|
||||
///
|
||||
/// By default, exa will display the information about files passed in as
|
||||
/// command-line arguments, with one file per entry. However, if a directory
|
||||
/// is passed in, exa assumes that the user wants to see its contents, rather
|
||||
/// than the directory itself.
|
||||
///
|
||||
/// This can get annoying sometimes: if a user does `exa ~/Downloads/img-*`
|
||||
/// to see the details of every file starting with `img-`, any directories
|
||||
/// that happen to start with the same will be listed after the files at
|
||||
/// the end in a separate block. By listing directories as files, their
|
||||
/// directory status will be ignored, and both will be listed side-by-side.
|
||||
///
|
||||
/// These two modes have recursive analogues in the “recurse” and “tree”
|
||||
/// modes. Here, instead of just listing the directories, exa will descend
|
||||
/// 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)]
|
||||
pub enum DirAction {
|
||||
|
||||
/// This directory should be listed along with the regular files, instead
|
||||
/// of having its contents queried.
|
||||
AsFile,
|
||||
|
||||
/// This directory should not be listed, and should instead be opened and
|
||||
/// *its* files listed separately. This is the default behaviour.
|
||||
List,
|
||||
|
||||
/// This directory should be listed along with the regular files, and then
|
||||
/// its contents should be listed afterward. The recursive contents of
|
||||
/// *those* contents are dictated by the options argument.
|
||||
Recurse(RecurseOptions),
|
||||
}
|
||||
|
||||
impl DirAction {
|
||||
|
||||
/// Gets the recurse options, if this dir action has any.
|
||||
pub fn recurse_options(&self) -> Option<RecurseOptions> {
|
||||
match *self {
|
||||
DirAction::Recurse(o) => Some(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to treat directories as regular files or not.
|
||||
pub fn treat_dirs_as_files(&self) -> bool {
|
||||
match *self {
|
||||
DirAction::AsFile => true,
|
||||
DirAction::Recurse(o) => o.tree,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The options that determine how to recurse into a directory.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct RecurseOptions {
|
||||
|
||||
/// Whether recursion should be done as a tree or as multiple individual
|
||||
/// views of files.
|
||||
pub tree: bool,
|
||||
|
||||
/// The maximum number of times that recursion should descend to, if one
|
||||
/// is specified.
|
||||
pub max_depth: Option<usize>,
|
||||
}
|
||||
|
||||
impl RecurseOptions {
|
||||
|
||||
/// Returns whether a directory of the given depth would be too deep.
|
||||
pub fn is_too_deep(&self, depth: usize) -> bool {
|
||||
match self.max_depth {
|
||||
None => false,
|
||||
Some(d) => d <= depth
|
||||
}
|
||||
}
|
||||
}
|
@ -1,76 +1,299 @@
|
||||
//! Getting the Git status of files and directories.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use git2;
|
||||
|
||||
use fs::fields as f;
|
||||
|
||||
|
||||
/// Container of Git statuses for all the files in this folder's Git repository.
|
||||
pub struct Git {
|
||||
/// A **Git cache** is assembled based on the user’s input arguments.
|
||||
///
|
||||
/// This uses vectors to avoid the overhead of hashing: it’s not worth it when the
|
||||
/// expected number of Git repositories per exa invocation is 0 or 1...
|
||||
pub struct GitCache {
|
||||
|
||||
/// A list of discovered Git repositories and their paths.
|
||||
repos: Vec<GitRepo>,
|
||||
|
||||
/// Paths that we’ve confirmed do not have Git repositories underneath them.
|
||||
misses: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl GitCache {
|
||||
pub fn has_anything_for(&self, index: &Path) -> bool {
|
||||
self.repos.iter().any(|e| e.has_path(index))
|
||||
}
|
||||
|
||||
pub fn get(&self, index: &Path, prefix_lookup: bool) -> f::Git {
|
||||
self.repos.iter()
|
||||
.find(|e| e.has_path(index))
|
||||
.map(|repo| repo.search(index, prefix_lookup))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
use std::iter::FromIterator;
|
||||
impl FromIterator<PathBuf> for GitCache {
|
||||
fn from_iter<I: IntoIterator<Item=PathBuf>>(iter: I) -> Self {
|
||||
let iter = iter.into_iter();
|
||||
let mut git = GitCache {
|
||||
repos: Vec::with_capacity(iter.size_hint().0),
|
||||
misses: Vec::new(),
|
||||
};
|
||||
|
||||
for path in iter {
|
||||
if git.misses.contains(&path) {
|
||||
debug!("Skipping {:?} because it already came back Gitless", path);
|
||||
}
|
||||
else if git.repos.iter().any(|e| e.has_path(&path)) {
|
||||
debug!("Skipping {:?} because we already queried it", path);
|
||||
}
|
||||
else {
|
||||
match GitRepo::discover(path) {
|
||||
Ok(r) => {
|
||||
if let Some(r2) = git.repos.iter_mut().find(|e| e.has_workdir(&r.workdir)) {
|
||||
debug!("Adding to existing repo (workdir matches with {:?})", r2.workdir);
|
||||
r2.extra_paths.push(r.original_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Discovered new Git repo");
|
||||
git.repos.push(r);
|
||||
},
|
||||
Err(miss) => git.misses.push(miss),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
git
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// A **Git repository** is one we’ve discovered somewhere on the filesystem.
|
||||
pub struct GitRepo {
|
||||
|
||||
/// The queryable contents of the repository: either a `git2` repo, or the
|
||||
/// cached results from when we queried it last time.
|
||||
contents: Mutex<GitContents>,
|
||||
|
||||
/// The working directory of this repository.
|
||||
/// This is used to check whether two repositories are the same.
|
||||
workdir: PathBuf,
|
||||
|
||||
/// The path that was originally checked to discover this repository.
|
||||
/// This is as important as the extra_paths (it gets checked first), but
|
||||
/// is separate to avoid having to deal with a non-empty Vec.
|
||||
original_path: PathBuf,
|
||||
|
||||
/// Any other paths that were checked only to result in this same
|
||||
/// repository.
|
||||
extra_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// A repository’s queried state.
|
||||
enum GitContents {
|
||||
|
||||
/// All the interesting Git stuff goes through this.
|
||||
Before { repo: git2::Repository },
|
||||
|
||||
/// Temporary value used in `repo_to_statuses` so we can move the
|
||||
/// repository out of the `Before` variant.
|
||||
Processing,
|
||||
|
||||
/// The data we’ve extracted from the repository, but only after we’ve
|
||||
/// actually done so.
|
||||
After { statuses: Git }
|
||||
}
|
||||
|
||||
impl GitRepo {
|
||||
|
||||
/// Searches through this repository for a path (to a file or directory,
|
||||
/// depending on the prefix-lookup flag) and returns its Git status.
|
||||
///
|
||||
/// Actually querying the `git2` repository for the mapping of paths to
|
||||
/// Git statuses is only done once, and gets cached so we don't need to
|
||||
/// re-query the entire repository the times after that.
|
||||
///
|
||||
/// The temporary `Processing` enum variant is used after the `git2`
|
||||
/// repository is moved out, but before the results have been moved in!
|
||||
/// See https://stackoverflow.com/q/45985827/3484614
|
||||
fn search(&self, index: &Path, prefix_lookup: bool) -> f::Git {
|
||||
use self::GitContents::*;
|
||||
use std::mem::replace;
|
||||
|
||||
let mut contents = self.contents.lock().unwrap();
|
||||
if let After { ref statuses } = *contents {
|
||||
debug!("Git repo {:?} has been found in cache", &self.workdir);
|
||||
return statuses.status(index, prefix_lookup);
|
||||
}
|
||||
|
||||
debug!("Querying Git repo {:?} for the first time", &self.workdir);
|
||||
let repo = replace(&mut *contents, Processing).inner_repo();
|
||||
let statuses = repo_to_statuses(&repo, &self.workdir);
|
||||
let result = statuses.status(index, prefix_lookup);
|
||||
let _processing = replace(&mut *contents, After { statuses });
|
||||
result
|
||||
}
|
||||
|
||||
/// Whether this repository has the given working directory.
|
||||
fn has_workdir(&self, path: &Path) -> bool {
|
||||
self.workdir == path
|
||||
}
|
||||
|
||||
/// Whether this repository cares about the given path at all.
|
||||
fn has_path(&self, path: &Path) -> bool {
|
||||
path.starts_with(&self.original_path) || self.extra_paths.iter().any(|e| path.starts_with(e))
|
||||
}
|
||||
|
||||
/// Searches for a Git repository at any point above the given path.
|
||||
/// Returns the original buffer if none is found.
|
||||
fn discover(path: PathBuf) -> Result<GitRepo, PathBuf> {
|
||||
info!("Searching for Git repository above {:?}", path);
|
||||
let repo = match git2::Repository::discover(&path) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Error discovering Git repositories: {:?}", e);
|
||||
return Err(path);
|
||||
}
|
||||
};
|
||||
|
||||
match repo.workdir().map(|wd| wd.to_path_buf()) {
|
||||
Some(workdir) => {
|
||||
let contents = Mutex::new(GitContents::Before { repo });
|
||||
Ok(GitRepo { contents, workdir, original_path: path, extra_paths: Vec::new() })
|
||||
},
|
||||
None => {
|
||||
warn!("Repository has no workdir?");
|
||||
Err(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl GitContents {
|
||||
/// Assumes that the repository hasn’t been queried, and extracts it
|
||||
/// (consuming the value) if it has. This is needed because the entire
|
||||
/// enum variant gets replaced when a repo is queried (see above).
|
||||
fn inner_repo(self) -> git2::Repository {
|
||||
if let GitContents::Before { repo } = self {
|
||||
repo
|
||||
}
|
||||
else {
|
||||
unreachable!("Tried to extract a non-Repository")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates through a repository’s statuses, consuming it and returning the
|
||||
/// mapping of files to their Git status.
|
||||
/// We will have already used the working directory at this point, so it gets
|
||||
/// passed in rather than deriving it from the `Repository` again.
|
||||
fn repo_to_statuses(repo: &git2::Repository, workdir: &Path) -> Git {
|
||||
let mut statuses = Vec::new();
|
||||
|
||||
info!("Getting Git statuses for repo with workdir {:?}", workdir);
|
||||
match repo.statuses(None) {
|
||||
Ok(es) => {
|
||||
for e in es.iter() {
|
||||
let path = workdir.join(Path::new(e.path().unwrap()));
|
||||
let elem = (path, e.status());
|
||||
statuses.push(elem);
|
||||
}
|
||||
},
|
||||
Err(e) => error!("Error looking up Git statuses: {:?}", e),
|
||||
}
|
||||
|
||||
Git { statuses }
|
||||
}
|
||||
|
||||
// The `repo.statuses` call above takes a long time. exa debug output:
|
||||
//
|
||||
// 20.311276 INFO:exa::fs::feature::git: Getting Git statuses for repo with workdir "/vagrant/"
|
||||
// 20.799610 DEBUG:exa::output::table: Getting Git status for file "./Cargo.toml"
|
||||
//
|
||||
// Even inserting another logging line immediately afterwards doesn't make it
|
||||
// look any faster.
|
||||
|
||||
|
||||
/// Container of Git statuses for all the files in this folder’s Git repository.
|
||||
struct Git {
|
||||
statuses: Vec<(PathBuf, git2::Status)>,
|
||||
}
|
||||
|
||||
impl Git {
|
||||
|
||||
/// Discover a Git repository on or above this directory, scanning it for
|
||||
/// the files' statuses if one is found.
|
||||
pub fn scan(path: &Path) -> Result<Git, git2::Error> {
|
||||
let repo = git2::Repository::discover(path)?;
|
||||
let workdir = match repo.workdir() {
|
||||
Some(w) => w,
|
||||
None => return Ok(Git { statuses: vec![] }), // bare repo
|
||||
};
|
||||
|
||||
let statuses = repo.statuses(None)?.iter()
|
||||
.map(|e| (workdir.join(Path::new(e.path().unwrap())), e.status()))
|
||||
.collect();
|
||||
|
||||
Ok(Git { statuses: statuses })
|
||||
/// Get either the file or directory status for the given path.
|
||||
/// “Prefix lookup” means that it should report an aggregate status of all
|
||||
/// paths starting with the given prefix (in other words, a directory).
|
||||
fn status(&self, index: &Path, prefix_lookup: bool) -> f::Git {
|
||||
if prefix_lookup { self.dir_status(index) }
|
||||
else { self.file_status(index) }
|
||||
}
|
||||
|
||||
/// Get the status for the file at the given path, if present.
|
||||
pub fn status(&self, path: &Path) -> f::Git {
|
||||
let status = self.statuses.iter()
|
||||
.find(|p| p.0.as_path() == path);
|
||||
match status {
|
||||
Some(&(_, s)) => f::Git { staged: index_status(s), unstaged: working_tree_status(s) },
|
||||
None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified }
|
||||
}
|
||||
/// Get the status for the file at the given path.
|
||||
fn file_status(&self, file: &Path) -> f::Git {
|
||||
let path = reorient(file);
|
||||
self.statuses.iter()
|
||||
.find(|p| p.0.as_path() == path)
|
||||
.map(|&(_, s)| f::Git { staged: index_status(s), unstaged: working_tree_status(s) })
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the combined status for all the files whose paths begin with the
|
||||
/// path that gets passed in. This is used for getting the status of
|
||||
/// directories, which don't really have an 'official' status.
|
||||
pub fn dir_status(&self, dir: &Path) -> f::Git {
|
||||
/// directories, which don’t really have an ‘official’ status.
|
||||
fn dir_status(&self, dir: &Path) -> f::Git {
|
||||
let path = reorient(dir);
|
||||
let s = self.statuses.iter()
|
||||
.filter(|p| p.0.starts_with(dir))
|
||||
.filter(|p| p.0.starts_with(&path))
|
||||
.fold(git2::Status::empty(), |a, b| a | b.1);
|
||||
|
||||
f::Git { staged: index_status(s), unstaged: working_tree_status(s) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a path to an absolute path based on the current directory.
|
||||
/// Paths need to be absolute for them to be compared properly, otherwise
|
||||
/// you’d ask a repo about “./README.md” but it only knows about
|
||||
/// “/vagrant/REAMDE.md”, prefixed by the workdir.
|
||||
fn reorient(path: &Path) -> PathBuf {
|
||||
use std::env::current_dir;
|
||||
// I’m not 100% on this func tbh
|
||||
match current_dir() {
|
||||
Err(_) => Path::new(".").join(&path),
|
||||
Ok(dir) => dir.join(&path),
|
||||
}.canonicalize().unwrap() // errors can be ignored here because they only occur if
|
||||
// the path does not exist / a component is not a folder
|
||||
}
|
||||
|
||||
/// The character to display if the file has been modified, but not staged.
|
||||
fn working_tree_status(status: git2::Status) -> f::GitStatus {
|
||||
match status {
|
||||
s if s.contains(git2::STATUS_WT_NEW) => f::GitStatus::New,
|
||||
s if s.contains(git2::STATUS_WT_MODIFIED) => f::GitStatus::Modified,
|
||||
s if s.contains(git2::STATUS_WT_DELETED) => f::GitStatus::Deleted,
|
||||
s if s.contains(git2::STATUS_WT_RENAMED) => f::GitStatus::Renamed,
|
||||
s if s.contains(git2::STATUS_WT_TYPECHANGE) => f::GitStatus::TypeChange,
|
||||
_ => f::GitStatus::NotModified,
|
||||
s if s.contains(git2::Status::WT_NEW) => f::GitStatus::New,
|
||||
s if s.contains(git2::Status::WT_MODIFIED) => f::GitStatus::Modified,
|
||||
s if s.contains(git2::Status::WT_DELETED) => f::GitStatus::Deleted,
|
||||
s if s.contains(git2::Status::WT_RENAMED) => f::GitStatus::Renamed,
|
||||
s if s.contains(git2::Status::WT_TYPECHANGE) => f::GitStatus::TypeChange,
|
||||
s if s.contains(git2::Status::IGNORED) => f::GitStatus::Ignored,
|
||||
_ => f::GitStatus::NotModified,
|
||||
}
|
||||
}
|
||||
|
||||
/// The character to display if the file has been modified, and the change
|
||||
/// The character to display if the file has been modified and the change
|
||||
/// has been staged.
|
||||
fn index_status(status: git2::Status) -> f::GitStatus {
|
||||
match status {
|
||||
s if s.contains(git2::STATUS_INDEX_NEW) => f::GitStatus::New,
|
||||
s if s.contains(git2::STATUS_INDEX_MODIFIED) => f::GitStatus::Modified,
|
||||
s if s.contains(git2::STATUS_INDEX_DELETED) => f::GitStatus::Deleted,
|
||||
s if s.contains(git2::STATUS_INDEX_RENAMED) => f::GitStatus::Renamed,
|
||||
s if s.contains(git2::STATUS_INDEX_TYPECHANGE) => f::GitStatus::TypeChange,
|
||||
_ => f::GitStatus::NotModified,
|
||||
s if s.contains(git2::Status::INDEX_NEW) => f::GitStatus::New,
|
||||
s if s.contains(git2::Status::INDEX_MODIFIED) => f::GitStatus::Modified,
|
||||
s if s.contains(git2::Status::INDEX_DELETED) => f::GitStatus::Deleted,
|
||||
s if s.contains(git2::Status::INDEX_RENAMED) => f::GitStatus::Renamed,
|
||||
s if s.contains(git2::Status::INDEX_TYPECHANGE) => f::GitStatus::TypeChange,
|
||||
_ => f::GitStatus::NotModified,
|
||||
}
|
||||
}
|
||||
|
196
src/fs/feature/ignore.rs
Normal file
196
src/fs/feature/ignore.rs
Normal file
@ -0,0 +1,196 @@
|
||||
//! Ignoring globs in `.gitignore` files.
|
||||
//!
|
||||
//! This uses a cache because the file with the globs in might not be the same
|
||||
//! directory that we’re listing!
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use fs::filter::IgnorePatterns;
|
||||
|
||||
|
||||
/// An **ignore cache** holds sets of glob patterns paired with the
|
||||
/// directories that they should be ignored underneath. Believe it or not,
|
||||
/// that’s a valid English sentence.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct IgnoreCache {
|
||||
entries: RwLock<Vec<(PathBuf, IgnorePatterns)>>
|
||||
}
|
||||
|
||||
impl IgnoreCache {
|
||||
pub fn new() -> IgnoreCache {
|
||||
IgnoreCache::default()
|
||||
}
|
||||
|
||||
pub fn discover_underneath(&self, path: &Path) {
|
||||
let mut path = Some(path);
|
||||
let mut entries = self.entries.write().unwrap();
|
||||
|
||||
while let Some(p) = path {
|
||||
if p.components().next().is_none() { break }
|
||||
|
||||
let ignore_file = p.join(".gitignore");
|
||||
if ignore_file.is_file() {
|
||||
debug!("Found a .gitignore file: {:?}", ignore_file);
|
||||
if let Ok(mut file) = File::open(ignore_file) {
|
||||
let mut contents = String::new();
|
||||
|
||||
match file.read_to_string(&mut contents) {
|
||||
Ok(_) => {
|
||||
let patterns = file_lines_to_patterns(contents.lines());
|
||||
entries.push((p.into(), patterns));
|
||||
}
|
||||
Err(e) => debug!("Failed to read a .gitignore: {:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
debug!("Found no .gitignore file at {:?}", ignore_file);
|
||||
}
|
||||
|
||||
path = p.parent();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ignored(&self, suspect: &Path) -> bool {
|
||||
let entries = self.entries.read().unwrap();
|
||||
entries.iter().any(|&(ref base_path, ref patterns)| {
|
||||
if let Ok(suffix) = suspect.strip_prefix(&base_path) {
|
||||
patterns.is_ignored_path(suffix)
|
||||
}
|
||||
else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn file_lines_to_patterns<'a, I>(iter: I) -> IgnorePatterns
|
||||
where I: Iterator<Item=&'a str>
|
||||
{
|
||||
let iter = iter.filter(|el| !el.is_empty());
|
||||
let iter = iter.filter(|el| !el.starts_with('#'));
|
||||
|
||||
// TODO: Figure out if this should trim whitespace or not
|
||||
|
||||
// Errors are currently being ignored... not a good look
|
||||
IgnorePatterns::parse_from_iter(iter).0
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_nothing() {
|
||||
use std::iter::empty;
|
||||
let (patterns, _) = IgnorePatterns::parse_from_iter(empty());
|
||||
assert_eq!(patterns, file_lines_to_patterns(empty()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_some_globs() {
|
||||
let stuff = vec![ "*.mp3", "README.md" ];
|
||||
let reals = vec![ "*.mp3", "README.md" ];
|
||||
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
||||
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_some_comments() {
|
||||
let stuff = vec![ "*.mp3", "# I am a comment!", "#", "README.md" ];
|
||||
let reals = vec![ "*.mp3", "README.md" ];
|
||||
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
||||
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_some_blank_lines() {
|
||||
let stuff = vec![ "*.mp3", "", "", "README.md" ];
|
||||
let reals = vec![ "*.mp3", "README.md" ];
|
||||
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
||||
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_some_whitespacey_lines() {
|
||||
let stuff = vec![ " *.mp3", " ", " a ", "README.md " ];
|
||||
let reals = vec![ " *.mp3", " ", " a ", "README.md " ];
|
||||
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
||||
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
||||
}
|
||||
|
||||
|
||||
fn test_cache(dir: &'static str, pats: Vec<&str>) -> IgnoreCache {
|
||||
IgnoreCache { entries: RwLock::new(vec![ (dir.into(), IgnorePatterns::parse_from_iter(pats.into_iter()).0) ]) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_cache_ignores_nothing() {
|
||||
let ignores = IgnoreCache::default();
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/usr/bin/drinking")));
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("target/debug/exa")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_nonempty_cache_ignores_some_things() {
|
||||
let ignores = test_cache("/vagrant", vec![ "target" ]);
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/src")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/target")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_some_globs() {
|
||||
let ignores = test_cache("/vagrant", vec![ "*.ipr", "*.iws", ".docker" ]);
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/exa.ipr")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/exa.iws")));
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/exa.iwiwal")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/.docker")));
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/exa.docker")));
|
||||
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/srcode/exa.ipr")));
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/srcode/exa.iws")));
|
||||
}
|
||||
|
||||
#[test] #[ignore]
|
||||
fn ignore_relatively() {
|
||||
let ignores = test_cache(".", vec![ "target" ]);
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
||||
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("./.target")));
|
||||
}
|
||||
|
||||
#[test] #[ignore]
|
||||
fn ignore_relatively_sometimes() {
|
||||
let ignores = test_cache(".", vec![ "project/target" ]);
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("./target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
||||
}
|
||||
|
||||
#[test] #[ignore]
|
||||
fn ignore_relatively_absolutely() {
|
||||
let ignores = test_cache(".", vec![ "/project/target" ]);
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("./target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
||||
}
|
||||
|
||||
#[test] #[ignore] // not 100% sure if dot works this way...
|
||||
fn ignore_relatively_absolutely_dot() {
|
||||
let ignores = test_cache(".", vec![ "./project/target" ]);
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("./target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
||||
}
|
||||
}
|
@ -1,26 +1,31 @@
|
||||
// Extended attribute support
|
||||
pub mod xattr;
|
||||
pub mod ignore;
|
||||
|
||||
// Git support
|
||||
|
||||
#[cfg(feature="git")] mod git;
|
||||
#[cfg(feature="git")] pub use self::git::Git;
|
||||
|
||||
#[cfg(not(feature="git"))] pub struct Git;
|
||||
#[cfg(not(feature="git"))] use std::path::Path;
|
||||
#[cfg(not(feature="git"))] use fs::fields;
|
||||
#[cfg(feature="git")] pub mod git;
|
||||
|
||||
#[cfg(not(feature="git"))]
|
||||
impl Git {
|
||||
pub fn scan(_: &Path) -> Result<Git, ()> {
|
||||
Err(())
|
||||
pub mod git {
|
||||
use std::iter::FromIterator;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use fs::fields as f;
|
||||
|
||||
|
||||
pub struct GitCache;
|
||||
|
||||
impl FromIterator<PathBuf> for GitCache {
|
||||
fn from_iter<I: IntoIterator<Item=PathBuf>>(_iter: I) -> Self {
|
||||
GitCache
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self, _: &Path) -> fields::Git {
|
||||
panic!("Tried to access a Git repo without Git support!");
|
||||
}
|
||||
impl GitCache {
|
||||
pub fn has_anything_for(&self, _index: &Path) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn dir_status(&self, path: &Path) -> fields::Git {
|
||||
self.status(path)
|
||||
pub fn get(&self, _index: &Path, _prefix_lookup: bool) -> f::Git {
|
||||
panic!("Tried to query a Git cache, but Git support is disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ mod lister {
|
||||
FollowSymlinks::No => 0x0000,
|
||||
};
|
||||
|
||||
Lister { c_flags: c_flags }
|
||||
Lister { c_flags }
|
||||
}
|
||||
|
||||
pub fn translate_attribute_name(&self, input: &[u8]) -> String {
|
||||
@ -203,7 +203,7 @@ mod lister {
|
||||
|
||||
impl Lister {
|
||||
pub fn new(follow_symlinks: FollowSymlinks) -> Lister {
|
||||
Lister { follow_symlinks: follow_symlinks }
|
||||
Lister { follow_symlinks }
|
||||
}
|
||||
|
||||
pub fn translate_attribute_name(&self, input: &[u8]) -> String {
|
||||
|
@ -1,3 +1,4 @@
|
||||
|
||||
//! Wrapper types for the values returned from `File`s.
|
||||
//!
|
||||
//! The methods of `File` that return information about the entry on the
|
||||
@ -166,7 +167,7 @@ pub struct DeviceIDs {
|
||||
|
||||
|
||||
/// One of a file’s timestamps (created, accessed, or modified).
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Time {
|
||||
pub seconds: time_t,
|
||||
pub nanoseconds: time_t,
|
||||
@ -196,6 +197,9 @@ pub enum GitStatus {
|
||||
|
||||
/// A file that’s had its type (such as the file permissions) changed.
|
||||
TypeChange,
|
||||
|
||||
/// A file that’s ignored (that matches a line in .gitignore)
|
||||
Ignored,
|
||||
}
|
||||
|
||||
/// A file’s complete Git status. It’s possible to make changes to a file, add
|
||||
@ -206,10 +210,11 @@ pub struct Git {
|
||||
pub unstaged: GitStatus,
|
||||
}
|
||||
|
||||
impl Git {
|
||||
use std::default::Default;
|
||||
impl Default for Git {
|
||||
|
||||
/// Create a Git status for a file with nothing done to it.
|
||||
pub fn empty() -> Git {
|
||||
fn default() -> Git {
|
||||
Git { staged: GitStatus::NotModified, unstaged: GitStatus::NotModified }
|
||||
}
|
||||
}
|
||||
|
238
src/fs/file.rs
238
src/fs/file.rs
@ -1,13 +1,15 @@
|
||||
//! Files, and methods and fields to access their metadata.
|
||||
|
||||
use std::fs;
|
||||
use std::fs::{self, metadata};
|
||||
use std::io::Error as IOError;
|
||||
use std::io::Result as IOResult;
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{UNIX_EPOCH, Duration};
|
||||
|
||||
use fs::dir::Dir;
|
||||
use fs::fields as f;
|
||||
use options::Misfire;
|
||||
|
||||
|
||||
/// A **File** is a wrapper around one of Rust's Path objects, along with
|
||||
@ -19,7 +21,7 @@ use fs::fields as f;
|
||||
/// start and hold on to all the information.
|
||||
pub struct File<'dir> {
|
||||
|
||||
/// The filename portion of this file's path, including the extension.
|
||||
/// The filename portion of this file’s path, including the extension.
|
||||
///
|
||||
/// This is used to compare against certain filenames (such as checking if
|
||||
/// it’s “Makefile” or something) and to highlight only the filename in
|
||||
@ -33,67 +35,98 @@ pub struct File<'dir> {
|
||||
|
||||
/// The path that begat this file.
|
||||
///
|
||||
/// Even though the file's name is extracted, the path needs to be kept
|
||||
/// around, as certain operations involve looking up the file's absolute
|
||||
/// location (such as the Git status, or searching for compiled files).
|
||||
/// Even though the file’s name is extracted, the path needs to be kept
|
||||
/// around, as certain operations involve looking up the file’s absolute
|
||||
/// location (such as searching for compiled files) or using its original
|
||||
/// path (following a symlink).
|
||||
pub path: PathBuf,
|
||||
|
||||
/// A cached `metadata` call for this file.
|
||||
/// A cached `metadata` (`stat`) call for this file.
|
||||
///
|
||||
/// This too is queried multiple times, and is *not* cached by the OS, as
|
||||
/// it could easily change between invocations - but exa is so short-lived
|
||||
/// it could easily change between invocations — but exa is so short-lived
|
||||
/// it's better to just cache it.
|
||||
pub metadata: fs::Metadata,
|
||||
|
||||
/// A reference to the directory that contains this file, if present.
|
||||
/// A reference to the directory that contains this file, if any.
|
||||
///
|
||||
/// Filenames that get passed in on the command-line directly will have no
|
||||
/// parent directory reference - although they technically have one on the
|
||||
/// filesystem, we'll never need to look at it, so it'll be `None`.
|
||||
/// parent directory reference — although they technically have one on the
|
||||
/// filesystem, we’ll never need to look at it, so it’ll be `None`.
|
||||
/// However, *directories* that get passed in will produce files that
|
||||
/// contain a reference to it, which is used in certain operations (such
|
||||
/// as looking up a file's Git status).
|
||||
/// as looking up compiled files).
|
||||
pub parent_dir: Option<&'dir Dir>,
|
||||
|
||||
/// Whether this is one of the two `--all all` directories, `.` and `..`.
|
||||
///
|
||||
/// Unlike all other entries, these are not returned as part of the
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl<'dir> File<'dir> {
|
||||
pub fn new<PD, FN>(path: PathBuf, parent_dir: PD, filename: FN) -> IOResult<File<'dir>>
|
||||
pub fn from_args<PD, FN>(path: PathBuf, parent_dir: PD, filename: FN) -> IOResult<File<'dir>>
|
||||
where PD: Into<Option<&'dir Dir>>,
|
||||
FN: Into<Option<String>>
|
||||
{
|
||||
let parent_dir = parent_dir.into();
|
||||
let metadata = fs::symlink_metadata(&path)?;
|
||||
let name = filename.into().unwrap_or_else(|| File::filename(&path));
|
||||
let ext = File::ext(&path);
|
||||
|
||||
Ok(File { path, parent_dir, metadata, ext, name })
|
||||
debug!("Statting file {:?}", &path);
|
||||
let metadata = fs::symlink_metadata(&path)?;
|
||||
let is_all_all = false;
|
||||
|
||||
Ok(File { path, parent_dir, metadata, ext, name, is_all_all })
|
||||
}
|
||||
|
||||
pub fn new_aa_current(parent_dir: &'dir Dir) -> IOResult<File<'dir>> {
|
||||
let path = parent_dir.path.to_path_buf();
|
||||
let ext = File::ext(&path);
|
||||
|
||||
debug!("Statting file {:?}", &path);
|
||||
let metadata = fs::symlink_metadata(&path)?;
|
||||
let is_all_all = true;
|
||||
|
||||
Ok(File { path, parent_dir: Some(parent_dir), metadata, ext, name: ".".to_string(), is_all_all })
|
||||
}
|
||||
|
||||
pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> IOResult<File<'dir>> {
|
||||
let ext = File::ext(&path);
|
||||
|
||||
debug!("Statting file {:?}", &path);
|
||||
let metadata = fs::symlink_metadata(&path)?;
|
||||
let is_all_all = true;
|
||||
|
||||
Ok(File { path, parent_dir: Some(parent_dir), metadata, ext, name: "..".to_string(), is_all_all })
|
||||
}
|
||||
|
||||
/// A file’s name is derived from its string. This needs to handle directories
|
||||
/// such as `/` or `..`, which have no `file_name` component. So instead, just
|
||||
/// use the last component as the name.
|
||||
pub fn filename(path: &Path) -> String {
|
||||
match path.components().next_back() {
|
||||
Some(back) => back.as_os_str().to_string_lossy().to_string(),
|
||||
None => path.display().to_string(), // use the path as fallback
|
||||
if let Some(back) = path.components().next_back() {
|
||||
back.as_os_str().to_string_lossy().to_string()
|
||||
}
|
||||
else {
|
||||
// use the path as fallback
|
||||
error!("Path {:?} has no last component", path);
|
||||
path.display().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract an extension from a file path, if one is present, in lowercase.
|
||||
///
|
||||
/// The extension is the series of characters after the last dot. This
|
||||
/// deliberately counts dotfiles, so the ".git" folder has the extension "git".
|
||||
/// deliberately counts dotfiles, so the “.git” folder has the extension “git”.
|
||||
///
|
||||
/// ASCII lowercasing is used because these extensions are only compared
|
||||
/// against a pre-compiled list of extensions which are known to only exist
|
||||
/// within ASCII, so it's alright.
|
||||
/// within ASCII, so it’s alright.
|
||||
fn ext(path: &Path) -> Option<String> {
|
||||
use std::ascii::AsciiExt;
|
||||
|
||||
let name = match path.file_name() {
|
||||
Some(f) => f.to_string_lossy().to_string(),
|
||||
None => return None,
|
||||
};
|
||||
let name = path.file_name().map(|f| f.to_string_lossy().to_string())?;
|
||||
|
||||
name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
|
||||
}
|
||||
@ -103,25 +136,41 @@ impl<'dir> File<'dir> {
|
||||
self.metadata.is_dir()
|
||||
}
|
||||
|
||||
/// If this file is a directory on the filesystem, then clone its
|
||||
/// `PathBuf` for use in one of our own `Dir` objects, and read a list of
|
||||
/// its contents.
|
||||
///
|
||||
/// Returns an IO error upon failure, but this shouldn't be used to check
|
||||
/// if a `File` is a directory or not! For that, just use `is_directory()`.
|
||||
pub fn to_dir(&self, scan_for_git: bool) -> IOResult<Dir> {
|
||||
Dir::read_dir(self.path.clone(), scan_for_git)
|
||||
/// Whether this file is a directory, or a symlink pointing to a directory.
|
||||
pub fn points_to_directory(&self) -> bool {
|
||||
if self.is_directory() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.is_link() {
|
||||
let target = self.link_target();
|
||||
if let FileTarget::Ok(target) = target {
|
||||
return target.points_to_directory();
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this file is a regular file on the filesystem - that is, not a
|
||||
/// If this file is a directory on the filesystem, then clone its
|
||||
/// `PathBuf` for use in one of our own `Dir` values, and read a list of
|
||||
/// its contents.
|
||||
///
|
||||
/// Returns an IO error upon failure, but this shouldn’t be used to check
|
||||
/// if a `File` is a directory or not! For that, just use `is_directory()`.
|
||||
pub fn to_dir(&self) -> IOResult<Dir> {
|
||||
Dir::read_dir(self.path.clone())
|
||||
}
|
||||
|
||||
/// Whether this file is a regular file on the filesystem — that is, not a
|
||||
/// directory, a link, or anything else treated specially.
|
||||
pub fn is_file(&self) -> bool {
|
||||
self.metadata.is_file()
|
||||
}
|
||||
|
||||
/// Whether this file is both a regular file *and* executable for the
|
||||
/// current user. Executable files have different semantics than
|
||||
/// executable directories, and so should be highlighted differently.
|
||||
/// current user. An executable file has a different purpose from an
|
||||
/// executable directory, so they should be highlighted differently.
|
||||
pub fn is_executable_file(&self) -> bool {
|
||||
let bit = modes::USER_EXECUTE;
|
||||
self.is_file() && (self.metadata.permissions().mode() & bit) == bit
|
||||
@ -153,7 +202,7 @@ impl<'dir> File<'dir> {
|
||||
}
|
||||
|
||||
|
||||
/// Re-prefixes the path pointed to by this file, if it's a symlink, to
|
||||
/// Re-prefixes the path pointed to by this file, if it’s a symlink, to
|
||||
/// make it an absolute path that can be accessed from whichever
|
||||
/// directory exa is being run from.
|
||||
fn reorient_target_path(&self, path: &Path) -> PathBuf {
|
||||
@ -184,9 +233,10 @@ impl<'dir> File<'dir> {
|
||||
pub fn link_target(&self) -> FileTarget<'dir> {
|
||||
|
||||
// We need to be careful to treat the path actually pointed to by
|
||||
// this file -- which could be absolute or relative -- to the path
|
||||
// we actually look up and turn into a `File` -- which needs to be
|
||||
// this file — which could be absolute or relative — to the path
|
||||
// we actually look up and turn into a `File` — which needs to be
|
||||
// absolute to be accessible from any directory.
|
||||
debug!("Reading link {:?}", &self.path);
|
||||
let path = match fs::read_link(&self.path) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return FileTarget::Err(e),
|
||||
@ -196,28 +246,31 @@ impl<'dir> File<'dir> {
|
||||
|
||||
// Use plain `metadata` instead of `symlink_metadata` - we *want* to
|
||||
// follow links.
|
||||
if let Ok(metadata) = fs::metadata(&absolute_path) {
|
||||
let ext = File::ext(&path);
|
||||
let name = File::filename(&path);
|
||||
FileTarget::Ok(File { parent_dir: None, path, ext, metadata, name })
|
||||
}
|
||||
else {
|
||||
FileTarget::Broken(path)
|
||||
match fs::metadata(&absolute_path) {
|
||||
Ok(metadata) => {
|
||||
let ext = File::ext(&path);
|
||||
let name = File::filename(&path);
|
||||
FileTarget::Ok(Box::new(File { parent_dir: None, path, ext, metadata, name, is_all_all: false }))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error following link {:?}: {:#?}", &path, e);
|
||||
FileTarget::Broken(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This file's number of hard links.
|
||||
/// This file’s number of hard links.
|
||||
///
|
||||
/// It also reports whether this is both a regular file, and a file with
|
||||
/// multiple links. This is important, because a file with multiple links
|
||||
/// is uncommon, while you can come across directories and other types
|
||||
/// is uncommon, while you come across directories and other types
|
||||
/// with multiple links much more often. Thus, it should get highlighted
|
||||
/// more attentively.
|
||||
pub fn links(&self) -> f::Links {
|
||||
let count = self.metadata.nlink();
|
||||
|
||||
f::Links {
|
||||
count: count,
|
||||
count,
|
||||
multiple: self.is_file() && count > 1,
|
||||
}
|
||||
}
|
||||
@ -274,27 +327,23 @@ impl<'dir> File<'dir> {
|
||||
}
|
||||
|
||||
/// This file’s last modified timestamp.
|
||||
pub fn modified_time(&self) -> f::Time {
|
||||
f::Time {
|
||||
seconds: self.metadata.mtime(),
|
||||
nanoseconds: self.metadata.mtime_nsec()
|
||||
}
|
||||
pub fn modified_time(&self) -> Duration {
|
||||
self.metadata.modified().unwrap().duration_since(UNIX_EPOCH).unwrap()
|
||||
}
|
||||
|
||||
/// This file’s created timestamp.
|
||||
pub fn created_time(&self) -> f::Time {
|
||||
f::Time {
|
||||
seconds: self.metadata.ctime(),
|
||||
nanoseconds: self.metadata.ctime_nsec()
|
||||
}
|
||||
/// This file’s last changed timestamp.
|
||||
pub fn changed_time(&self) -> Duration {
|
||||
Duration::new(self.metadata.ctime() as u64, self.metadata.ctime_nsec() as u32)
|
||||
}
|
||||
|
||||
/// This file’s last accessed timestamp.
|
||||
pub fn accessed_time(&self) -> f::Time {
|
||||
f::Time {
|
||||
seconds: self.metadata.atime(),
|
||||
nanoseconds: self.metadata.atime_nsec()
|
||||
}
|
||||
pub fn accessed_time(&self) -> Duration {
|
||||
self.metadata.accessed().unwrap().duration_since(UNIX_EPOCH).unwrap()
|
||||
}
|
||||
|
||||
/// This file’s created timestamp.
|
||||
pub fn created_time(&self) -> Duration {
|
||||
self.metadata.created().unwrap().duration_since(UNIX_EPOCH).unwrap()
|
||||
}
|
||||
|
||||
/// This file’s ‘type’.
|
||||
@ -368,28 +417,6 @@ impl<'dir> File<'dir> {
|
||||
pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
|
||||
choices.contains(&&self.name[..])
|
||||
}
|
||||
|
||||
/// This file's Git status as two flags: one for staged changes, and the
|
||||
/// other for unstaged changes.
|
||||
///
|
||||
/// This requires looking at the `git` field of this file's parent
|
||||
/// directory, so will not work if this file has just been passed in on
|
||||
/// the command line.
|
||||
pub fn git_status(&self) -> f::Git {
|
||||
use std::env::current_dir;
|
||||
|
||||
match self.parent_dir {
|
||||
None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
|
||||
Some(d) => {
|
||||
let cwd = match current_dir() {
|
||||
Err(_) => Path::new(".").join(&self.path),
|
||||
Ok(dir) => dir.join(&self.path),
|
||||
};
|
||||
|
||||
d.git_status(&cwd, self.is_directory())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -404,7 +431,7 @@ impl<'a> AsRef<File<'a>> for File<'a> {
|
||||
pub enum FileTarget<'dir> {
|
||||
|
||||
/// The symlink pointed at a file that exists.
|
||||
Ok(File<'dir>),
|
||||
Ok(Box<File<'dir>>),
|
||||
|
||||
/// The symlink pointed at a file that does not exist. Holds the path
|
||||
/// where the file would be, if it existed.
|
||||
@ -433,6 +460,41 @@ impl<'dir> FileTarget<'dir> {
|
||||
}
|
||||
|
||||
|
||||
pub enum PlatformMetadata {
|
||||
ModifiedTime,
|
||||
ChangedTime,
|
||||
AccessedTime,
|
||||
CreatedTime,
|
||||
}
|
||||
|
||||
impl PlatformMetadata {
|
||||
pub fn check_supported(&self) -> Result<(), Misfire> {
|
||||
use std::env::temp_dir;
|
||||
let result = match self {
|
||||
// Call the functions that return a Result to see if it works
|
||||
PlatformMetadata::AccessedTime => metadata(temp_dir()).unwrap().accessed(),
|
||||
PlatformMetadata::ModifiedTime => metadata(temp_dir()).unwrap().modified(),
|
||||
PlatformMetadata::CreatedTime => metadata(temp_dir()).unwrap().created(),
|
||||
// We use the Unix API so we know it’s not available elsewhere
|
||||
PlatformMetadata::ChangedTime => {
|
||||
if cfg!(target_family = "unix") {
|
||||
return Ok(())
|
||||
} else {
|
||||
return Err(Misfire::Unsupported(
|
||||
// for consistency, this error message similar to the one Rust
|
||||
// use when created time is not available
|
||||
"status modified time is not available on this platform currently".to_string()));
|
||||
}
|
||||
},
|
||||
};
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Misfire::Unsupported(err.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// More readable aliases for the permission bits exposed by libc.
|
||||
#[allow(trivial_numeric_casts)]
|
||||
mod modes {
|
||||
|
410
src/fs/filter.rs
Normal file
410
src/fs/filter.rs
Normal file
@ -0,0 +1,410 @@
|
||||
//! Filtering and sorting the list of files before displaying them.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::iter::FromIterator;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::Path;
|
||||
|
||||
use glob;
|
||||
use natord;
|
||||
|
||||
use fs::File;
|
||||
use fs::DotFilter;
|
||||
|
||||
|
||||
/// The **file filter** processes a list of files before displaying them to
|
||||
/// the user, by removing files they don’t want to see, and putting the list
|
||||
/// in the desired order.
|
||||
///
|
||||
/// Usually a user does not want to see *every* file in the list. The most
|
||||
/// common case is to remove files starting with `.`, which are designated
|
||||
/// as ‘hidden’ files.
|
||||
///
|
||||
/// The special files `.` and `..` files are not actually filtered out, but
|
||||
/// need to be inserted into the list, in a special case.
|
||||
///
|
||||
/// 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)]
|
||||
pub struct FileFilter {
|
||||
|
||||
/// Whether directories should be listed first, and other types of file
|
||||
/// second. Some users prefer it like this.
|
||||
pub list_dirs_first: bool,
|
||||
|
||||
/// The metadata field to sort by.
|
||||
pub sort_field: SortField,
|
||||
|
||||
/// Whether to reverse the sorting order. This would sort the largest
|
||||
/// files first, or files starting with Z, or the most-recently-changed
|
||||
/// ones, depending on the sort field.
|
||||
pub reverse: bool,
|
||||
|
||||
/// Whether to only show directories.
|
||||
pub only_dirs: bool,
|
||||
|
||||
/// Which invisible “dot” files to include when listing a directory.
|
||||
///
|
||||
/// Files starting with a single “.” are used to determine “system” or
|
||||
/// “configuration” files that should not be displayed in a regular
|
||||
/// directory listing, and the directory entries “.” and “..” are
|
||||
/// considered extra-special.
|
||||
///
|
||||
/// This came about more or less by a complete historical accident,
|
||||
/// when the original `ls` tried to hide `.` and `..`:
|
||||
/// https://plus.google.com/+RobPikeTheHuman/posts/R58WgWwN9jp
|
||||
///
|
||||
/// When one typed ls, however, these files appeared, so either Ken or
|
||||
/// Dennis added a simple test to the program. It was in assembler then,
|
||||
/// but the code in question was equivalent to something like this:
|
||||
/// if (name[0] == '.') continue;
|
||||
/// This statement was a little shorter than what it should have been,
|
||||
/// which is:
|
||||
/// if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
/// but hey, it was easy.
|
||||
///
|
||||
/// Two things resulted.
|
||||
///
|
||||
/// First, a bad precedent was set. A lot of other lazy programmers
|
||||
/// introduced bugs by making the same simplification. Actual files
|
||||
/// beginning with periods are often skipped when they should be counted.
|
||||
///
|
||||
/// Second, and much worse, the idea of a "hidden" or "dot" file was
|
||||
/// created. As a consequence, more lazy programmers started dropping
|
||||
/// files into everyone's home directory. I don't have all that much
|
||||
/// stuff installed on the machine I'm using to type this, but my home
|
||||
/// directory has about a hundred dot files and I don't even know what
|
||||
/// most of them are or whether they're still needed. Every file name
|
||||
/// evaluation that goes through my home directory is slowed down by
|
||||
/// this accumulated sludge.
|
||||
pub dot_filter: DotFilter,
|
||||
|
||||
/// Glob patterns to ignore. Any file name that matches *any* of these
|
||||
/// patterns won’t be displayed in the list.
|
||||
pub ignore_patterns: IgnorePatterns,
|
||||
|
||||
/// Whether to ignore Git-ignored patterns.
|
||||
/// This is implemented completely separately from the actual Git
|
||||
/// repository scanning — a `.gitignore` file will still be scanned even
|
||||
/// if there’s no `.git` folder present.
|
||||
pub git_ignore: GitIgnore,
|
||||
}
|
||||
|
||||
|
||||
impl FileFilter {
|
||||
/// Remove every file in the given vector that does *not* pass the
|
||||
/// filter predicate for files found inside a directory.
|
||||
pub fn filter_child_files(&self, files: &mut Vec<File>) {
|
||||
files.retain(|f| !self.ignore_patterns.is_ignored(&f.name));
|
||||
|
||||
if self.only_dirs {
|
||||
files.retain(|f| f.is_directory());
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove every file in the given vector that does *not* pass the
|
||||
/// filter predicate for file names specified on the command-line.
|
||||
///
|
||||
/// The rules are different for these types of files than the other
|
||||
/// type because the ignore rules can be used with globbing. For
|
||||
/// example, running `exa -I='*.tmp' .vimrc` shouldn’t filter out the
|
||||
/// dotfile, because it’s been directly specified. But running
|
||||
/// `exa -I='*.ogg' music/*` should filter out the ogg files obtained
|
||||
/// from the glob, even though the globbing is done by the shell!
|
||||
pub fn filter_argument_files(&self, files: &mut Vec<File>) {
|
||||
files.retain(|f| !self.ignore_patterns.is_ignored(&f.name));
|
||||
}
|
||||
|
||||
/// Sort the files in the given vector based on the sort field option.
|
||||
pub fn sort_files<'a, F>(&self, files: &mut Vec<F>)
|
||||
where F: AsRef<File<'a>> {
|
||||
|
||||
files.sort_by(|a, b| self.sort_field.compare_files(a.as_ref(), b.as_ref()));
|
||||
|
||||
if self.reverse {
|
||||
files.reverse();
|
||||
}
|
||||
|
||||
if self.list_dirs_first {
|
||||
// This relies on the fact that `sort_by` is *stable*: it will keep
|
||||
// adjacent elements next to each other.
|
||||
files.sort_by(|a, b| b.as_ref().is_directory().cmp(&a.as_ref().is_directory()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// User-supplied field to sort by.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum SortField {
|
||||
|
||||
/// Don’t apply any sorting. This is usually used as an optimisation in
|
||||
/// scripts, where the order doesn’t matter.
|
||||
Unsorted,
|
||||
|
||||
/// The file name. This is the default sorting.
|
||||
Name(SortCase),
|
||||
|
||||
/// The file’s extension, with extensionless files being listed first.
|
||||
Extension(SortCase),
|
||||
|
||||
/// The file’s size, in bytes.
|
||||
Size,
|
||||
|
||||
/// The file’s inode, which usually corresponds to the order in which
|
||||
/// files were created on the filesystem, more or less.
|
||||
FileInode,
|
||||
|
||||
/// The time the file was modified (the “mtime”).
|
||||
///
|
||||
/// As this is stored as a Unix timestamp, rather than a local time
|
||||
/// instance, the time zone does not matter and will only be used to
|
||||
/// display the timestamps, not compare them.
|
||||
ModifiedDate,
|
||||
|
||||
/// The time the file was accessed (the “atime”).
|
||||
///
|
||||
/// Oddly enough, this field rarely holds the *actual* accessed time.
|
||||
/// Recording a read time means writing to the file each time it’s read
|
||||
/// slows the whole operation down, so many systems will only update the
|
||||
/// timestamp in certain circumstances. This has become common enough that
|
||||
/// it’s now expected behaviour!
|
||||
/// http://unix.stackexchange.com/a/8842
|
||||
AccessedDate,
|
||||
|
||||
/// The time the file was changed (the “ctime”).
|
||||
///
|
||||
/// This field is used to mark the time when a file’s metadata
|
||||
/// changed -- its permissions, owners, or link count.
|
||||
///
|
||||
/// In original Unix, this was, however, meant as creation time.
|
||||
/// https://www.bell-labs.com/usr/dmr/www/cacm.html
|
||||
ChangedDate,
|
||||
|
||||
/// The time the file was created (the "btime" or "birthtime").
|
||||
CreatedDate,
|
||||
|
||||
/// The type of the file: directories, links, pipes, regular, files, etc.
|
||||
///
|
||||
/// Files are ordered according to the `PartialOrd` implementation of
|
||||
/// `fs::fields::Type`, so changing that will change this.
|
||||
FileType,
|
||||
|
||||
/// The “age” of the file, which is the time it was modified sorted
|
||||
/// backwards. The reverse of the `ModifiedDate` ordering!
|
||||
///
|
||||
/// It turns out that listing the most-recently-modified files first is a
|
||||
/// common-enough use case that it deserves its own variant. This would be
|
||||
/// implemented by just using the modified date and setting the reverse
|
||||
/// flag, but this would make reversing *that* output not work, which is
|
||||
/// bad, even though that’s kind of nonsensical. So it’s its own variant
|
||||
/// that can be reversed like usual.
|
||||
ModifiedAge,
|
||||
|
||||
/// The file's name, however if the name of the file begins with `.`
|
||||
/// ignore the leading `.` and then sort as Name
|
||||
NameMixHidden(SortCase),
|
||||
}
|
||||
|
||||
/// Whether a field should be sorted case-sensitively or case-insensitively.
|
||||
/// This determines which of the `natord` functions to use.
|
||||
///
|
||||
/// I kept on forgetting which one was sensitive and which one was
|
||||
/// insensitive. Would a case-sensitive sort put capital letters first because
|
||||
/// it takes the case of the letters into account, or intermingle them with
|
||||
/// 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)]
|
||||
pub enum SortCase {
|
||||
|
||||
/// Sort files case-sensitively with uppercase first, with ‘A’ coming
|
||||
/// before ‘a’.
|
||||
ABCabc,
|
||||
|
||||
/// Sort files case-insensitively, with ‘A’ being equal to ‘a’.
|
||||
AaBbCc,
|
||||
}
|
||||
|
||||
impl SortField {
|
||||
|
||||
/// Compares two files to determine the order they should be listed in,
|
||||
/// depending on the search field.
|
||||
///
|
||||
/// The `natord` crate is used here to provide a more *natural* sorting
|
||||
/// order than just sorting character-by-character. This splits filenames
|
||||
/// into groups between letters and numbers, and then sorts those blocks
|
||||
/// together, so `file10` will sort after `file9`, instead of before it
|
||||
/// because of the `1`.
|
||||
pub fn compare_files(self, a: &File, b: &File) -> Ordering {
|
||||
use self::SortCase::{ABCabc, AaBbCc};
|
||||
|
||||
match self {
|
||||
SortField::Unsorted => Ordering::Equal,
|
||||
|
||||
SortField::Name(ABCabc) => natord::compare(&a.name, &b.name),
|
||||
SortField::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name),
|
||||
|
||||
SortField::Size => a.metadata.len().cmp(&b.metadata.len()),
|
||||
SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
|
||||
SortField::ModifiedDate => a.modified_time().cmp(&b.modified_time()),
|
||||
SortField::AccessedDate => a.accessed_time().cmp(&b.accessed_time()),
|
||||
SortField::ChangedDate => a.changed_time().cmp(&b.changed_time()),
|
||||
SortField::CreatedDate => a.created_time().cmp(&b.created_time()),
|
||||
SortField::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a
|
||||
|
||||
SortField::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes
|
||||
Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
||||
order => order,
|
||||
},
|
||||
|
||||
SortField::Extension(ABCabc) => match a.ext.cmp(&b.ext) {
|
||||
Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
||||
order => order,
|
||||
},
|
||||
|
||||
SortField::Extension(AaBbCc) => match a.ext.cmp(&b.ext) {
|
||||
Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name),
|
||||
order => order,
|
||||
},
|
||||
|
||||
SortField::NameMixHidden(ABCabc) => natord::compare(
|
||||
SortField::strip_dot(&a.name),
|
||||
SortField::strip_dot(&b.name)
|
||||
),
|
||||
SortField::NameMixHidden(AaBbCc) => natord::compare_ignore_case(
|
||||
SortField::strip_dot(&a.name),
|
||||
SortField::strip_dot(&b.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_dot(n: &str) -> &str {
|
||||
if n.starts_with('.') {
|
||||
&n[1..]
|
||||
} else {
|
||||
n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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)]
|
||||
pub struct IgnorePatterns {
|
||||
patterns: Vec<glob::Pattern>,
|
||||
}
|
||||
|
||||
impl FromIterator<glob::Pattern> for IgnorePatterns {
|
||||
fn from_iter<I: IntoIterator<Item = glob::Pattern>>(iter: I) -> Self {
|
||||
IgnorePatterns { patterns: iter.into_iter().collect() }
|
||||
}
|
||||
}
|
||||
|
||||
impl IgnorePatterns {
|
||||
|
||||
/// Create a new list from the input glob strings, turning the inputs that
|
||||
/// are valid glob patterns into an IgnorePatterns. The inputs that don’t
|
||||
/// parse correctly are returned separately.
|
||||
pub fn parse_from_iter<'a, I: IntoIterator<Item = &'a str>>(iter: I) -> (Self, Vec<glob::PatternError>) {
|
||||
let iter = iter.into_iter();
|
||||
|
||||
// Almost all glob patterns are valid, so it’s worth pre-allocating
|
||||
// the vector with enough space for all of them.
|
||||
let mut patterns = match iter.size_hint() {
|
||||
(_, Some(count)) => Vec::with_capacity(count),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// Similarly, assume there won’t be any errors.
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for input in iter {
|
||||
match glob::Pattern::new(input) {
|
||||
Ok(pat) => patterns.push(pat),
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
|
||||
(IgnorePatterns { patterns }, errors)
|
||||
}
|
||||
|
||||
/// Create a new empty set of patterns that matches nothing.
|
||||
pub fn empty() -> IgnorePatterns {
|
||||
IgnorePatterns { patterns: Vec::new() }
|
||||
}
|
||||
|
||||
/// Test whether the given file should be hidden from the results.
|
||||
fn is_ignored(&self, file: &str) -> bool {
|
||||
self.patterns.iter().any(|p| p.matches(file))
|
||||
}
|
||||
|
||||
/// Test whether the given file should be hidden from the results.
|
||||
pub fn is_ignored_path(&self, file: &Path) -> bool {
|
||||
self.patterns.iter().any(|p| p.matches_path(file))
|
||||
}
|
||||
|
||||
// TODO(ogham): The fact that `is_ignored_path` is pub while `is_ignored`
|
||||
// isn’t probably means it’s in the wrong place
|
||||
}
|
||||
|
||||
|
||||
/// Whether to ignore or display files that are mentioned in `.gitignore` files.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum GitIgnore {
|
||||
|
||||
/// Ignore files that Git would ignore. This means doing a check for a
|
||||
/// `.gitignore` file, possibly recursively up the filesystem tree.
|
||||
CheckAndIgnore,
|
||||
|
||||
/// Display files, even if Git would ignore them.
|
||||
Off,
|
||||
}
|
||||
|
||||
// This is not fully baked yet. The `ignore` crate lists a lot more files that
|
||||
// we aren’t checking:
|
||||
//
|
||||
// > By default, all ignore files found are respected. This includes .ignore,
|
||||
// > .gitignore, .git/info/exclude and even your global gitignore globs,
|
||||
// > usually found in $XDG_CONFIG_HOME/git/ignore.
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ignores {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_matches_nothing() {
|
||||
let pats = IgnorePatterns::empty();
|
||||
assert_eq!(false, pats.is_ignored("nothing"));
|
||||
assert_eq!(false, 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"));
|
||||
}
|
||||
|
||||
#[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"));
|
||||
}
|
||||
|
||||
#[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"));
|
||||
}
|
||||
}
|
@ -2,7 +2,9 @@ mod dir;
|
||||
pub use self::dir::{Dir, DotFilter};
|
||||
|
||||
mod file;
|
||||
pub use self::file::{File, FileTarget};
|
||||
pub use self::file::{File, FileTarget, PlatformMetadata};
|
||||
|
||||
pub mod feature;
|
||||
pub mod fields;
|
||||
pub mod filter;
|
||||
pub mod dir_action;
|
||||
|
@ -4,10 +4,14 @@
|
||||
//! those are the only metadata that we have access to without reading the
|
||||
//! file’s contents.
|
||||
|
||||
use ansi_term::Style;
|
||||
|
||||
use fs::File;
|
||||
use output::file_name::FileColours;
|
||||
use output::icons::FileIcon;
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct FileExtensions;
|
||||
|
||||
impl FileExtensions {
|
||||
@ -15,50 +19,50 @@ impl FileExtensions {
|
||||
/// An “immediate” file is something that can be run or activated somehow
|
||||
/// in order to kick off the build of a project. It’s usually only present
|
||||
/// in directories full of source code.
|
||||
pub fn is_immediate(&self, file: &File) -> bool {
|
||||
file.name.starts_with("README") || file.name_is_one_of( &[
|
||||
fn is_immediate(&self, file: &File) -> bool {
|
||||
file.name.to_lowercase().starts_with("readme") || file.name_is_one_of( &[
|
||||
"Makefile", "Cargo.toml", "SConstruct", "CMakeLists.txt",
|
||||
"build.gradle", "Rakefile", "Gruntfile.js",
|
||||
"Gruntfile.coffee",
|
||||
"Gruntfile.coffee", "BUILD", "BUILD.bazel", "WORKSPACE", "build.xml"
|
||||
])
|
||||
}
|
||||
|
||||
pub fn is_image(&self, file: &File) -> bool {
|
||||
fn is_image(&self, file: &File) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif",
|
||||
"ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw",
|
||||
"svg", "stl", "eps", "dvi", "ps", "cbr",
|
||||
"cbz", "xpm", "ico", "cr2",
|
||||
"svg", "stl", "eps", "dvi", "ps", "cbr", "jpf",
|
||||
"cbz", "xpm", "ico", "cr2", "orf", "nef",
|
||||
])
|
||||
}
|
||||
|
||||
pub fn is_video(&self, file: &File) -> bool {
|
||||
fn is_video(&self, file: &File) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"avi", "flv", "m2v", "mkv", "mov", "mp4", "mpeg",
|
||||
"mpg", "ogm", "ogv", "vob", "wmv",
|
||||
"avi", "flv", "m2v", "m4v", "mkv", "mov", "mp4", "mpeg",
|
||||
"mpg", "ogm", "ogv", "vob", "wmv", "webm", "m2ts",
|
||||
])
|
||||
}
|
||||
|
||||
pub fn is_music(&self, file: &File) -> bool {
|
||||
fn is_music(&self, file: &File) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"aac", "m4a", "mp3", "ogg", "wma",
|
||||
"aac", "m4a", "mp3", "ogg", "wma", "mka", "opus",
|
||||
])
|
||||
}
|
||||
|
||||
// Lossless music, rather than any other kind of data...
|
||||
pub fn is_lossless(&self, file: &File) -> bool {
|
||||
fn is_lossless(&self, file: &File) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"alac", "ape", "flac", "wav",
|
||||
])
|
||||
}
|
||||
|
||||
pub fn is_crypto(&self, file: &File) -> bool {
|
||||
fn is_crypto(&self, file: &File) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"asc", "enc", "gpg", "pgp", "sig", "signature", "pfx", "p12",
|
||||
])
|
||||
}
|
||||
|
||||
pub fn is_document(&self, file: &File) -> bool {
|
||||
fn is_document(&self, file: &File) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"djvu", "doc", "docx", "dvi", "eml", "eps", "fotd",
|
||||
"odp", "odt", "pdf", "ppt", "pptx", "rtf",
|
||||
@ -66,20 +70,21 @@ impl FileExtensions {
|
||||
])
|
||||
}
|
||||
|
||||
pub fn is_compressed(&self, file: &File) -> bool {
|
||||
fn is_compressed(&self, file: &File) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"zip", "tar", "Z", "gz", "bz2", "a", "ar", "7z",
|
||||
"iso", "dmg", "tc", "rar", "par", "tgz",
|
||||
"zip", "tar", "Z", "z", "gz", "bz2", "a", "ar", "7z",
|
||||
"iso", "dmg", "tc", "rar", "par", "tgz", "xz", "txz",
|
||||
"lzma", "deb", "rpm", "zst",
|
||||
])
|
||||
}
|
||||
|
||||
pub fn is_temp(&self, file: &File) -> bool {
|
||||
fn is_temp(&self, file: &File) -> bool {
|
||||
file.name.ends_with('~')
|
||||
|| (file.name.starts_with('#') && file.name.ends_with('#'))
|
||||
|| file.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak" ])
|
||||
|| file.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak", "bk" ])
|
||||
}
|
||||
|
||||
pub fn is_compiled(&self, file: &File) -> bool {
|
||||
fn is_compiled(&self, file: &File) -> bool {
|
||||
if file.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc" ]) {
|
||||
true
|
||||
}
|
||||
@ -91,3 +96,36 @@ impl FileExtensions {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileColours for FileExtensions {
|
||||
fn colour_file(&self, file: &File) -> Option<Style> {
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
Some(match file {
|
||||
f if self.is_temp(f) => Fixed(244).normal(),
|
||||
f if self.is_immediate(f) => Yellow.bold().underline(),
|
||||
f if self.is_image(f) => Fixed(133).normal(),
|
||||
f if self.is_video(f) => Fixed(135).normal(),
|
||||
f if self.is_music(f) => Fixed(92).normal(),
|
||||
f if self.is_lossless(f) => Fixed(93).normal(),
|
||||
f if self.is_crypto(f) => Fixed(109).normal(),
|
||||
f if self.is_document(f) => Fixed(105).normal(),
|
||||
f if self.is_compressed(f) => Red.normal(),
|
||||
f if self.is_compiled(f) => Fixed(137).normal(),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FileIcon for FileExtensions {
|
||||
fn icon_file(&self, file: &File) -> Option<char> {
|
||||
use output::icons::Icons;
|
||||
|
||||
Some(match file {
|
||||
f if self.is_music(f) || self.is_lossless(f) => Icons::Audio.value(),
|
||||
f if self.is_image(f) => Icons::Image.value(),
|
||||
f if self.is_video(f) => Icons::Video.value(),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,86 +1,60 @@
|
||||
use getopts;
|
||||
//! Parsing the options for `DirAction`.
|
||||
|
||||
use options::misfire::Misfire;
|
||||
use options::parser::MatchedFlags;
|
||||
use options::{flags, Misfire};
|
||||
|
||||
use fs::dir_action::{DirAction, RecurseOptions};
|
||||
|
||||
/// What to do when encountering a directory?
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum DirAction {
|
||||
|
||||
/// This directory should be listed along with the regular files, instead
|
||||
/// of having its contents queried.
|
||||
AsFile,
|
||||
|
||||
/// This directory should not be listed, and should instead be opened and
|
||||
/// *its* files listed separately. This is the default behaviour.
|
||||
List,
|
||||
|
||||
/// This directory should be listed along with the regular files, and then
|
||||
/// its contents should be listed afterward. The recursive contents of
|
||||
/// *those* contents are dictated by the options argument.
|
||||
Recurse(RecurseOptions),
|
||||
}
|
||||
|
||||
impl DirAction {
|
||||
|
||||
/// Determine which action to perform when trying to list a directory.
|
||||
pub fn deduce(matches: &getopts::Matches) -> Result<DirAction, Misfire> {
|
||||
let recurse = matches.opt_present("recurse");
|
||||
let list = matches.opt_present("list-dirs");
|
||||
let tree = matches.opt_present("tree");
|
||||
/// There are three possible actions, and they overlap somewhat: the
|
||||
/// `--tree` flag is another form of recursion, so those two are allowed
|
||||
/// to both be present, but the `--list-dirs` flag is used separately.
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<DirAction, Misfire> {
|
||||
let recurse = matches.has(&flags::RECURSE)?;
|
||||
let as_file = matches.has(&flags::LIST_DIRS)?;
|
||||
let tree = matches.has(&flags::TREE)?;
|
||||
|
||||
match (recurse, list, tree) {
|
||||
|
||||
// You can't --list-dirs along with --recurse or --tree because
|
||||
// they already automatically list directories.
|
||||
(true, true, _ ) => Err(Misfire::Conflict("recurse", "list-dirs")),
|
||||
(_, true, true ) => Err(Misfire::Conflict("tree", "list-dirs")),
|
||||
|
||||
(_ , _, true ) => Ok(DirAction::Recurse(RecurseOptions::deduce(matches, true)?)),
|
||||
(true, false, false) => Ok(DirAction::Recurse(RecurseOptions::deduce(matches, false)?)),
|
||||
(false, true, _ ) => Ok(DirAction::AsFile),
|
||||
(false, false, _ ) => Ok(DirAction::List),
|
||||
if matches.is_strict() {
|
||||
// Early check for --level when it wouldn’t do anything
|
||||
if !recurse && !tree && matches.count(&flags::LEVEL) > 0 {
|
||||
return Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
|
||||
}
|
||||
else if recurse && as_file {
|
||||
return Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS));
|
||||
}
|
||||
else if tree && as_file {
|
||||
return Err(Misfire::Conflict(&flags::TREE, &flags::LIST_DIRS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the recurse options, if this dir action has any.
|
||||
pub fn recurse_options(&self) -> Option<RecurseOptions> {
|
||||
match *self {
|
||||
DirAction::Recurse(opts) => Some(opts),
|
||||
_ => None,
|
||||
if tree {
|
||||
Ok(DirAction::Recurse(RecurseOptions::deduce(matches, true)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to treat directories as regular files or not.
|
||||
pub fn treat_dirs_as_files(&self) -> bool {
|
||||
match *self {
|
||||
DirAction::AsFile => true,
|
||||
DirAction::Recurse(RecurseOptions { tree, .. }) => tree,
|
||||
_ => false,
|
||||
else if recurse {
|
||||
Ok(DirAction::Recurse(RecurseOptions::deduce(matches, false)?))
|
||||
}
|
||||
else if as_file {
|
||||
Ok(DirAction::AsFile)
|
||||
}
|
||||
else {
|
||||
Ok(DirAction::List)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The options that determine how to recurse into a directory.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct RecurseOptions {
|
||||
|
||||
/// Whether recursion should be done as a tree or as multiple individual
|
||||
/// views of files.
|
||||
pub tree: bool,
|
||||
|
||||
/// The maximum number of times that recursion should descend to, if one
|
||||
/// is specified.
|
||||
pub max_depth: Option<usize>,
|
||||
}
|
||||
|
||||
impl RecurseOptions {
|
||||
|
||||
/// Determine which files should be recursed into.
|
||||
pub fn deduce(matches: &getopts::Matches, tree: bool) -> Result<RecurseOptions, Misfire> {
|
||||
let max_depth = if let Some(level) = matches.opt_str("level") {
|
||||
match level.parse() {
|
||||
/// Determine which files should be recursed into, based on the `--level`
|
||||
/// flag’s value, and whether the `--tree` flag was passed, which was
|
||||
/// determined earlier. The maximum level should be a number, and this
|
||||
/// will fail with an `Err` if it isn’t.
|
||||
pub fn deduce(matches: &MatchedFlags, tree: bool) -> Result<RecurseOptions, Misfire> {
|
||||
let max_depth = if let Some(level) = matches.get(&flags::LEVEL)? {
|
||||
match level.to_string_lossy().parse() {
|
||||
Ok(l) => Some(l),
|
||||
Err(e) => return Err(Misfire::FailedParse(e)),
|
||||
}
|
||||
@ -91,14 +65,62 @@ impl RecurseOptions {
|
||||
|
||||
Ok(RecurseOptions { tree, max_depth })
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a directory of the given depth would be too deep.
|
||||
pub fn is_too_deep(&self, depth: usize) -> bool {
|
||||
match self.max_depth {
|
||||
None => false,
|
||||
Some(d) => {
|
||||
d <= depth
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use options::flags;
|
||||
use options::parser::Flag;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
use options::parser::Arg;
|
||||
use options::test::parse_for_test;
|
||||
use options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[&flags::RECURSE, &flags::LIST_DIRS, &flags::TREE, &flags::LEVEL ];
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
assert_eq!(result, $result);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Default behaviour
|
||||
test!(empty: DirAction <- []; Both => Ok(DirAction::List));
|
||||
|
||||
// Listing files as directories
|
||||
test!(dirs_short: DirAction <- ["-d"]; Both => Ok(DirAction::AsFile));
|
||||
test!(dirs_long: DirAction <- ["--list-dirs"]; Both => Ok(DirAction::AsFile));
|
||||
|
||||
// Recursing
|
||||
use self::DirAction::Recurse;
|
||||
test!(rec_short: DirAction <- ["-R"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: None })));
|
||||
test!(rec_long: DirAction <- ["--recurse"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: None })));
|
||||
test!(rec_lim_short: DirAction <- ["-RL4"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(4) })));
|
||||
test!(rec_lim_short_2: DirAction <- ["-RL=5"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(5) })));
|
||||
test!(rec_lim_long: DirAction <- ["--recurse", "--level", "666"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(666) })));
|
||||
test!(rec_lim_long_2: DirAction <- ["--recurse", "--level=0118"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(118) })));
|
||||
test!(tree: DirAction <- ["--tree"]; Both => Ok(Recurse(RecurseOptions { tree: true, max_depth: None })));
|
||||
test!(rec_tree: DirAction <- ["--recurse", "--tree"]; Both => Ok(Recurse(RecurseOptions { tree: true, max_depth: None })));
|
||||
test!(rec_short_tree: DirAction <- ["-TR"]; Both => Ok(Recurse(RecurseOptions { tree: true, max_depth: None })));
|
||||
|
||||
// Overriding --list-dirs, --recurse, and --tree
|
||||
test!(dirs_recurse: DirAction <- ["--list-dirs", "--recurse"]; Last => Ok(Recurse(RecurseOptions { tree: false, max_depth: None })));
|
||||
test!(dirs_tree: DirAction <- ["--list-dirs", "--tree"]; Last => Ok(Recurse(RecurseOptions { tree: true, max_depth: None })));
|
||||
test!(just_level: DirAction <- ["--level=4"]; Last => Ok(DirAction::List));
|
||||
|
||||
test!(dirs_recurse_2: DirAction <- ["--list-dirs", "--recurse"]; Complain => Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS)));
|
||||
test!(dirs_tree_2: DirAction <- ["--list-dirs", "--tree"]; Complain => Err(Misfire::Conflict(&flags::TREE, &flags::LIST_DIRS)));
|
||||
test!(just_level_2: DirAction <- ["--level=4"]; Complain => Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)));
|
||||
|
||||
|
||||
// Overriding levels
|
||||
test!(overriding_1: DirAction <- ["-RL=6", "-L=7"]; Last => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(7) })));
|
||||
test!(overriding_2: DirAction <- ["-RL=6", "-L=7"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'L'), Flag::Short(b'L'))));
|
||||
}
|
||||
|
@ -1,305 +1,302 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
//! Parsing the options for `FileFilter`.
|
||||
|
||||
use getopts;
|
||||
use glob;
|
||||
use natord;
|
||||
use fs::{DotFilter, PlatformMetadata};
|
||||
use fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore};
|
||||
|
||||
use fs::File;
|
||||
use fs::DotFilter;
|
||||
use options::misfire::Misfire;
|
||||
use options::{flags, Misfire};
|
||||
use options::parser::MatchedFlags;
|
||||
|
||||
|
||||
/// The **file filter** processes a vector of files before outputting them,
|
||||
/// filtering and sorting the files depending on the user’s command-line
|
||||
/// flags.
|
||||
#[derive(Default, PartialEq, Debug, Clone)]
|
||||
pub struct FileFilter {
|
||||
|
||||
/// Whether directories should be listed first, and other types of file
|
||||
/// second. Some users prefer it like this.
|
||||
pub list_dirs_first: bool,
|
||||
|
||||
/// The metadata field to sort by.
|
||||
pub sort_field: SortField,
|
||||
|
||||
/// Whether to reverse the sorting order. This would sort the largest
|
||||
/// files first, or files starting with Z, or the most-recently-changed
|
||||
/// ones, depending on the sort field.
|
||||
pub reverse: bool,
|
||||
|
||||
/// Which invisible “dot” files to include when listing a directory.
|
||||
///
|
||||
/// Files starting with a single “.” are used to determine “system” or
|
||||
/// “configuration” files that should not be displayed in a regular
|
||||
/// directory listing, and the directory entries “.” and “..” are
|
||||
/// considered extra-special.
|
||||
///
|
||||
/// This came about more or less by a complete historical accident,
|
||||
/// when the original `ls` tried to hide `.` and `..`:
|
||||
/// https://plus.google.com/+RobPikeTheHuman/posts/R58WgWwN9jp
|
||||
///
|
||||
/// When one typed ls, however, these files appeared, so either Ken or
|
||||
/// Dennis added a simple test to the program. It was in assembler then,
|
||||
/// but the code in question was equivalent to something like this:
|
||||
/// if (name[0] == '.') continue;
|
||||
/// This statement was a little shorter than what it should have been,
|
||||
/// which is:
|
||||
/// if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
/// but hey, it was easy.
|
||||
///
|
||||
/// Two things resulted.
|
||||
///
|
||||
/// First, a bad precedent was set. A lot of other lazy programmers
|
||||
/// introduced bugs by making the same simplification. Actual files
|
||||
/// beginning with periods are often skipped when they should be counted.
|
||||
///
|
||||
/// Second, and much worse, the idea of a "hidden" or "dot" file was
|
||||
/// created. As a consequence, more lazy programmers started dropping
|
||||
/// files into everyone's home directory. I don't have all that much
|
||||
/// stuff installed on the machine I'm using to type this, but my home
|
||||
/// directory has about a hundred dot files and I don't even know what
|
||||
/// most of them are or whether they're still needed. Every file name
|
||||
/// evaluation that goes through my home directory is slowed down by
|
||||
/// this accumulated sludge.
|
||||
pub dot_filter: DotFilter,
|
||||
|
||||
/// Glob patterns to ignore. Any file name that matches *any* of these
|
||||
/// patterns won't be displayed in the list.
|
||||
ignore_patterns: IgnorePatterns,
|
||||
}
|
||||
|
||||
impl FileFilter {
|
||||
|
||||
/// Determines the set of file filter options to use, based on the user’s
|
||||
/// command-line arguments.
|
||||
pub fn deduce(matches: &getopts::Matches) -> Result<FileFilter, Misfire> {
|
||||
/// Determines which of all the file filter options to use.
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<FileFilter, Misfire> {
|
||||
Ok(FileFilter {
|
||||
list_dirs_first: matches.opt_present("group-directories-first"),
|
||||
reverse: matches.opt_present("reverse"),
|
||||
list_dirs_first: matches.has(&flags::DIRS_FIRST)?,
|
||||
reverse: matches.has(&flags::REVERSE)?,
|
||||
only_dirs: matches.has(&flags::ONLY_DIRS)?,
|
||||
sort_field: SortField::deduce(matches)?,
|
||||
dot_filter: DotFilter::deduce(matches)?,
|
||||
ignore_patterns: IgnorePatterns::deduce(matches)?,
|
||||
git_ignore: GitIgnore::deduce(matches)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove every file in the given vector that does *not* pass the
|
||||
/// filter predicate for files found inside a directory.
|
||||
pub fn filter_child_files(&self, files: &mut Vec<File>) {
|
||||
files.retain(|f| !self.ignore_patterns.is_ignored(f));
|
||||
}
|
||||
|
||||
/// Remove every file in the given vector that does *not* pass the
|
||||
/// filter predicate for file names specified on the command-line.
|
||||
///
|
||||
/// The rules are different for these types of files than the other
|
||||
/// type because the ignore rules can be used with globbing. For
|
||||
/// example, running "exa -I='*.tmp' .vimrc" shouldn't filter out the
|
||||
/// dotfile, because it's been directly specified. But running
|
||||
/// "exa -I='*.ogg' music/*" should filter out the ogg files obtained
|
||||
/// from the glob, even though the globbing is done by the shell!
|
||||
pub fn filter_argument_files(&self, files: &mut Vec<File>) {
|
||||
files.retain(|f| !self.ignore_patterns.is_ignored(f));
|
||||
}
|
||||
|
||||
/// Sort the files in the given vector based on the sort field option.
|
||||
pub fn sort_files<'a, F>(&self, files: &mut Vec<F>)
|
||||
where F: AsRef<File<'a>> {
|
||||
|
||||
files.sort_by(|a, b| self.compare_files(a.as_ref(), b.as_ref()));
|
||||
|
||||
if self.reverse {
|
||||
files.reverse();
|
||||
}
|
||||
|
||||
if self.list_dirs_first {
|
||||
// This relies on the fact that `sort_by` is stable.
|
||||
files.sort_by(|a, b| b.as_ref().is_directory().cmp(&a.as_ref().is_directory()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares two files to determine the order they should be listed in,
|
||||
/// depending on the search field.
|
||||
pub fn compare_files(&self, a: &File, b: &File) -> Ordering {
|
||||
use self::SortCase::{Sensitive, Insensitive};
|
||||
|
||||
match self.sort_field {
|
||||
SortField::Unsorted => Ordering::Equal,
|
||||
|
||||
SortField::Name(Sensitive) => natord::compare(&a.name, &b.name),
|
||||
SortField::Name(Insensitive) => natord::compare_ignore_case(&a.name, &b.name),
|
||||
|
||||
SortField::Size => a.metadata.len().cmp(&b.metadata.len()),
|
||||
SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
|
||||
SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()),
|
||||
SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()),
|
||||
SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()),
|
||||
|
||||
SortField::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes
|
||||
Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
||||
order => order,
|
||||
},
|
||||
|
||||
SortField::Extension(Sensitive) => match a.ext.cmp(&b.ext) {
|
||||
Ordering::Equal => natord::compare(&*a.name, &*b.name),
|
||||
order => order,
|
||||
},
|
||||
|
||||
SortField::Extension(Insensitive) => match a.ext.cmp(&b.ext) {
|
||||
Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name),
|
||||
order => order,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// User-supplied field to sort by.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum SortField {
|
||||
|
||||
/// Don't apply any sorting. This is usually used as an optimisation in
|
||||
/// scripts, where the order doesn't matter.
|
||||
Unsorted,
|
||||
|
||||
/// The file name. This is the default sorting.
|
||||
Name(SortCase),
|
||||
|
||||
/// The file's extension, with extensionless files being listed first.
|
||||
Extension(SortCase),
|
||||
|
||||
/// The file's size.
|
||||
Size,
|
||||
|
||||
/// The file's inode. This is sometimes analogous to the order in which
|
||||
/// the files were created on the hard drive.
|
||||
FileInode,
|
||||
|
||||
/// The time at which this file was modified (the `mtime`).
|
||||
///
|
||||
/// As this is stored as a Unix timestamp, rather than a local time
|
||||
/// instance, the time zone does not matter and will only be used to
|
||||
/// display the timestamps, not compare them.
|
||||
ModifiedDate,
|
||||
|
||||
/// The time at this file was accessed (the `atime`).
|
||||
///
|
||||
/// Oddly enough, this field rarely holds the *actual* accessed time.
|
||||
/// Recording a read time means writing to the file each time it’s read
|
||||
/// slows the whole operation down, so many systems will only update the
|
||||
/// timestamp in certain circumstances. This has become common enough that
|
||||
/// it’s now expected behaviour for the `atime` field.
|
||||
/// http://unix.stackexchange.com/a/8842
|
||||
AccessedDate,
|
||||
|
||||
/// The time at which this file was changed or created (the `ctime`).
|
||||
///
|
||||
/// Contrary to the name, this field is used to mark the time when a
|
||||
/// file's metadata changed -- its permissions, owners, or link count.
|
||||
///
|
||||
/// In original Unix, this was, however, meant as creation time.
|
||||
/// https://www.bell-labs.com/usr/dmr/www/cacm.html
|
||||
CreatedDate,
|
||||
|
||||
/// The type of the file: directories, links, pipes, regular, files, etc.
|
||||
///
|
||||
/// Files are ordered according to the `PartialOrd` implementation of
|
||||
/// `fs::fields::Type`, so changing that will change this.
|
||||
FileType,
|
||||
}
|
||||
|
||||
/// Whether a field should be sorted case-sensitively or case-insensitively.
|
||||
///
|
||||
/// This determines which of the `natord` functions to use.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum SortCase {
|
||||
|
||||
/// Sort files case-sensitively with uppercase first, with ‘A’ coming
|
||||
/// before ‘a’.
|
||||
Sensitive,
|
||||
|
||||
/// Sort files case-insensitively, with ‘A’ being equal to ‘a’.
|
||||
Insensitive,
|
||||
}
|
||||
|
||||
impl Default for SortField {
|
||||
fn default() -> SortField {
|
||||
SortField::Name(SortCase::Sensitive)
|
||||
}
|
||||
}
|
||||
|
||||
impl SortField {
|
||||
|
||||
/// Determine the sort field to use, based on the presence of a “sort”
|
||||
/// argument. This will return `Err` if the option is there, but does not
|
||||
/// correspond to a valid field.
|
||||
fn deduce(matches: &getopts::Matches) -> Result<SortField, Misfire> {
|
||||
/// Determines which sort field to use based on the `--sort` argument.
|
||||
/// This argument’s value can be one of several flags, listed above.
|
||||
/// Returns the default sort field if none is given, or `Err` if the
|
||||
/// value doesn’t correspond to a sort field we know about.
|
||||
fn deduce(matches: &MatchedFlags) -> Result<SortField, Misfire> {
|
||||
let word = match matches.get(&flags::SORT)? {
|
||||
Some(w) => w,
|
||||
None => return Ok(SortField::default()),
|
||||
};
|
||||
|
||||
const SORTS: &[&str] = &[ "name", "Name", "size", "extension",
|
||||
"Extension", "modified", "accessed",
|
||||
"created", "inode", "type", "none" ];
|
||||
// Get String because we can’t match an OsStr
|
||||
let word = match word.to_str() {
|
||||
Some(ref w) => *w,
|
||||
None => return Err(Misfire::BadArgument(&flags::SORT, word.into()))
|
||||
};
|
||||
|
||||
if let Some(word) = matches.opt_str("sort") {
|
||||
match &*word {
|
||||
"name" | "filename" => Ok(SortField::Name(SortCase::Sensitive)),
|
||||
"Name" | "Filename" => Ok(SortField::Name(SortCase::Insensitive)),
|
||||
"size" | "filesize" => Ok(SortField::Size),
|
||||
"ext" | "extension" => Ok(SortField::Extension(SortCase::Sensitive)),
|
||||
"Ext" | "Extension" => Ok(SortField::Extension(SortCase::Insensitive)),
|
||||
"mod" | "modified" => Ok(SortField::ModifiedDate),
|
||||
"acc" | "accessed" => Ok(SortField::AccessedDate),
|
||||
"cr" | "created" => Ok(SortField::CreatedDate),
|
||||
"inode" => Ok(SortField::FileInode),
|
||||
"type" => Ok(SortField::FileType),
|
||||
"none" => Ok(SortField::Unsorted),
|
||||
field => Err(Misfire::bad_argument("sort", field, SORTS))
|
||||
}
|
||||
let field = match word {
|
||||
"name" | "filename" => SortField::Name(SortCase::AaBbCc),
|
||||
"Name" | "Filename" => SortField::Name(SortCase::ABCabc),
|
||||
".name" | ".filename" => SortField::NameMixHidden(SortCase::AaBbCc),
|
||||
".Name" | ".Filename" => SortField::NameMixHidden(SortCase::ABCabc),
|
||||
"size" | "filesize" => SortField::Size,
|
||||
"ext" | "extension" => SortField::Extension(SortCase::AaBbCc),
|
||||
"Ext" | "Extension" => SortField::Extension(SortCase::ABCabc),
|
||||
// “new” sorts oldest at the top and newest at the bottom; “old”
|
||||
// sorts newest at the top and oldest at the bottom. I think this
|
||||
// is the right way round to do this: “size” puts the smallest at
|
||||
// the top and the largest at the bottom, doesn’t it?
|
||||
"date" | "time" | "mod" | "modified" | "new" | "newest" => SortField::ModifiedDate,
|
||||
// Similarly, “age” means that files with the least age (the
|
||||
// newest files) get sorted at the top, and files with the most
|
||||
// age (the oldest) at the bottom.
|
||||
"age" | "old" | "oldest" => SortField::ModifiedAge,
|
||||
"ch" | "changed" => SortField::ChangedDate,
|
||||
"acc" | "accessed" => SortField::AccessedDate,
|
||||
"cr" | "created" => SortField::CreatedDate,
|
||||
"inode" => SortField::FileInode,
|
||||
"type" => SortField::FileType,
|
||||
"none" => SortField::Unsorted,
|
||||
_ => return Err(Misfire::BadArgument(&flags::SORT, word.into()))
|
||||
};
|
||||
|
||||
match SortField::to_platform_metadata(field) {
|
||||
Some(m) => match m.check_supported() {
|
||||
Ok(_) => Ok(field),
|
||||
Err(misfire) => Err(misfire),
|
||||
},
|
||||
None => Ok(field),
|
||||
}
|
||||
else {
|
||||
Ok(SortField::default())
|
||||
}
|
||||
|
||||
fn to_platform_metadata(field: Self) -> Option<PlatformMetadata> {
|
||||
match field {
|
||||
SortField::ModifiedDate => Some(PlatformMetadata::ModifiedTime),
|
||||
SortField::ChangedDate => Some(PlatformMetadata::ChangedTime),
|
||||
SortField::AccessedDate => Some(PlatformMetadata::AccessedTime),
|
||||
SortField::CreatedDate => Some(PlatformMetadata::CreatedTime),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// I’ve gone back and forth between whether to sort case-sensitively or
|
||||
// insensitively by default. The default string sort in most programming
|
||||
// languages takes each character’s ASCII value into account, sorting
|
||||
// “Documents” before “apps”, but there’s usually an option to ignore
|
||||
// characters’ case, putting “apps” before “Documents”.
|
||||
//
|
||||
// The argument for following case is that it’s easy to forget whether an item
|
||||
// begins with an uppercase or lowercase letter and end up having to scan both
|
||||
// the uppercase and lowercase sub-lists to find the item you want. If you
|
||||
// happen to pick the sublist it’s not in, it looks like it’s missing, which
|
||||
// is worse than if you just take longer to find it.
|
||||
// (https://ux.stackexchange.com/a/79266)
|
||||
//
|
||||
// The argument for ignoring case is that it makes exa sort files differently
|
||||
// from shells. A user would expect a directory’s files to be in the same
|
||||
// order if they used “exa ~/directory” or “exa ~/directory/*”, but exa sorts
|
||||
// them in the first case, and the shell in the second case, so they wouldn’t
|
||||
// be exactly the same if exa does something non-conventional.
|
||||
//
|
||||
// However, exa already sorts files differently: it uses natural sorting from
|
||||
// the natord crate, sorting the string “2” before “10” because the number’s
|
||||
// smaller, because that’s usually what the user expects to happen. Users will
|
||||
// name their files with numbers expecting them to be treated like numbers,
|
||||
// rather than lists of numeric characters.
|
||||
//
|
||||
// In the same way, users will name their files with letters expecting the
|
||||
// order of the letters to matter, rather than each letter’s character’s ASCII
|
||||
// value. So exa breaks from tradition and ignores case while sorting:
|
||||
// “apps” first, then “Documents”.
|
||||
//
|
||||
// You can get the old behaviour back by sorting with `--sort=Name`.
|
||||
|
||||
impl Default for SortField {
|
||||
fn default() -> SortField {
|
||||
SortField::Name(SortCase::AaBbCc)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl DotFilter {
|
||||
pub fn deduce(matches: &getopts::Matches) -> Result<DotFilter, Misfire> {
|
||||
let dots = match matches.opt_count("all") {
|
||||
0 => return Ok(DotFilter::JustFiles),
|
||||
1 => DotFilter::Dotfiles,
|
||||
_ => DotFilter::DotfilesAndDots,
|
||||
};
|
||||
|
||||
if matches.opt_present("tree") {
|
||||
Err(Misfire::Useless("all --all", true, "tree"))
|
||||
/// Determines the dot filter based on how many `--all` options were
|
||||
/// given: one will show dotfiles, but two will show `.` and `..` too.
|
||||
///
|
||||
/// It also checks for the `--tree` option in strict mode, because of a
|
||||
/// special case where `--tree --all --all` won’t work: listing the
|
||||
/// parent directory in tree mode would loop onto itself!
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<DotFilter, Misfire> {
|
||||
let count = matches.count(&flags::ALL);
|
||||
|
||||
if count == 0 {
|
||||
Ok(DotFilter::JustFiles)
|
||||
}
|
||||
else if count == 1 {
|
||||
Ok(DotFilter::Dotfiles)
|
||||
}
|
||||
else if matches.count(&flags::TREE) > 0 {
|
||||
Err(Misfire::TreeAllAll)
|
||||
}
|
||||
else if count >= 3 && matches.is_strict() {
|
||||
Err(Misfire::Conflict(&flags::ALL, &flags::ALL))
|
||||
}
|
||||
else {
|
||||
Ok(dots)
|
||||
Ok(DotFilter::DotfilesAndDots)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Default, Debug, Clone)]
|
||||
struct IgnorePatterns {
|
||||
patterns: Vec<glob::Pattern>,
|
||||
}
|
||||
|
||||
impl IgnorePatterns {
|
||||
/// Determines the set of file filter options to use, based on the user’s
|
||||
/// command-line arguments.
|
||||
pub fn deduce(matches: &getopts::Matches) -> Result<IgnorePatterns, Misfire> {
|
||||
let patterns = match matches.opt_str("ignore-glob") {
|
||||
None => Ok(Vec::new()),
|
||||
Some(is) => is.split('|').map(|a| glob::Pattern::new(a)).collect(),
|
||||
|
||||
/// Determines the set of glob patterns to use based on the
|
||||
/// `--ignore-patterns` argument’s value. This is a list of strings
|
||||
/// separated by pipe (`|`) characters, given in any order.
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<IgnorePatterns, Misfire> {
|
||||
|
||||
// If there are no inputs, we return a set of patterns that doesn’t
|
||||
// match anything, rather than, say, `None`.
|
||||
let inputs = match matches.get(&flags::IGNORE_GLOB)? {
|
||||
None => return Ok(IgnorePatterns::empty()),
|
||||
Some(is) => is,
|
||||
};
|
||||
|
||||
Ok(IgnorePatterns {
|
||||
patterns: patterns?,
|
||||
})
|
||||
}
|
||||
// Awkwardly, though, a glob pattern can be invalid, and we need to
|
||||
// deal with invalid patterns somehow.
|
||||
let (patterns, mut errors) = IgnorePatterns::parse_from_iter(inputs.to_string_lossy().split('|'));
|
||||
|
||||
fn is_ignored(&self, file: &File) -> bool {
|
||||
self.patterns.iter().any(|p| p.matches(&file.name))
|
||||
// It can actually return more than one glob error,
|
||||
// but we only use one. (TODO)
|
||||
match errors.pop() {
|
||||
Some(e) => Err(e.into()),
|
||||
None => Ok(patterns),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl GitIgnore {
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
|
||||
Ok(if matches.has(&flags::GIT_IGNORE)? { GitIgnore::CheckAndIgnore }
|
||||
else { GitIgnore::Off })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
use options::flags;
|
||||
use options::parser::Flag;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
use options::parser::Arg;
|
||||
use options::test::parse_for_test;
|
||||
use options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::TREE, &flags::IGNORE_GLOB, &flags::GIT_IGNORE ];
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
assert_eq!(result, $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod sort_fields {
|
||||
use super::*;
|
||||
|
||||
// Default behaviour
|
||||
test!(empty: SortField <- []; Both => Ok(SortField::default()));
|
||||
|
||||
// Sort field arguments
|
||||
test!(one_arg: SortField <- ["--sort=mod"]; Both => Ok(SortField::ModifiedDate));
|
||||
test!(one_long: SortField <- ["--sort=size"]; Both => Ok(SortField::Size));
|
||||
test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate));
|
||||
test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::AaBbCc)));
|
||||
test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::ABCabc)));
|
||||
test!(old: SortField <- ["--sort", "new"]; Both => Ok(SortField::ModifiedDate));
|
||||
test!(oldest: SortField <- ["--sort=newest"]; Both => Ok(SortField::ModifiedDate));
|
||||
test!(new: SortField <- ["--sort", "old"]; Both => Ok(SortField::ModifiedAge));
|
||||
test!(newest: SortField <- ["--sort=oldest"]; Both => Ok(SortField::ModifiedAge));
|
||||
test!(age: SortField <- ["-sage"]; Both => Ok(SortField::ModifiedAge));
|
||||
|
||||
test!(mix_hidden_lowercase: SortField <- ["--sort", ".name"]; Both => Ok(SortField::NameMixHidden(SortCase::AaBbCc)));
|
||||
test!(mix_hidden_uppercase: SortField <- ["--sort", ".Name"]; Both => Ok(SortField::NameMixHidden(SortCase::ABCabc)));
|
||||
|
||||
// Errors
|
||||
test!(error: SortField <- ["--sort=colour"]; Both => Err(Misfire::BadArgument(&flags::SORT, OsString::from("colour"))));
|
||||
|
||||
// Overriding
|
||||
test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate));
|
||||
test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"]; Last => Ok(SortField::Extension(SortCase::ABCabc)));
|
||||
test!(overridden_3: SortField <- ["--sort=cr", "--sort", "mod"]; Complain => Err(Misfire::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
|
||||
test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(Misfire::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
|
||||
}
|
||||
|
||||
|
||||
mod dot_filters {
|
||||
use super::*;
|
||||
|
||||
// Default behaviour
|
||||
test!(empty: DotFilter <- []; Both => Ok(DotFilter::JustFiles));
|
||||
|
||||
// --all
|
||||
test!(all: DotFilter <- ["--all"]; Both => Ok(DotFilter::Dotfiles));
|
||||
test!(all_all: DotFilter <- ["--all", "-a"]; Both => Ok(DotFilter::DotfilesAndDots));
|
||||
test!(all_all_2: DotFilter <- ["-aa"]; Both => Ok(DotFilter::DotfilesAndDots));
|
||||
|
||||
test!(all_all_3: DotFilter <- ["-aaa"]; Last => Ok(DotFilter::DotfilesAndDots));
|
||||
test!(all_all_4: DotFilter <- ["-aaa"]; Complain => Err(Misfire::Conflict(&flags::ALL, &flags::ALL)));
|
||||
|
||||
// --all and --tree
|
||||
test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles));
|
||||
test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(Misfire::TreeAllAll));
|
||||
test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(Misfire::TreeAllAll));
|
||||
}
|
||||
|
||||
|
||||
mod ignore_patterns {
|
||||
use super::*;
|
||||
use std::iter::FromIterator;
|
||||
use glob;
|
||||
|
||||
fn pat(string: &'static str) -> glob::Pattern {
|
||||
glob::Pattern::new(string).unwrap()
|
||||
}
|
||||
|
||||
// Various numbers of globs
|
||||
test!(none: IgnorePatterns <- []; Both => Ok(IgnorePatterns::empty()));
|
||||
test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ])));
|
||||
test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ])));
|
||||
test!(loads: IgnorePatterns <- ["-I*|?|.|*"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ])));
|
||||
|
||||
// Overriding
|
||||
test!(overridden: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.mp3") ])));
|
||||
test!(overridden_2: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.MP3") ])));
|
||||
test!(overridden_3: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
|
||||
test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
|
||||
}
|
||||
|
||||
|
||||
mod git_ignores {
|
||||
use super::*;
|
||||
|
||||
test!(off: GitIgnore <- []; Both => Ok(GitIgnore::Off));
|
||||
test!(on: GitIgnore <- ["--git-ignore"]; Both => Ok(GitIgnore::CheckAndIgnore));
|
||||
}
|
||||
}
|
||||
|
74
src/options/flags.rs
Normal file
74
src/options/flags.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use options::parser::{Arg, Args, Values, TakesValue};
|
||||
|
||||
|
||||
// exa options
|
||||
pub static VERSION: Arg = Arg { short: Some(b'v'), long: "version", takes_value: TakesValue::Forbidden };
|
||||
pub static HELP: Arg = Arg { short: Some(b'?'), long: "help", takes_value: TakesValue::Forbidden };
|
||||
|
||||
// display options
|
||||
pub static ONE_LINE: Arg = Arg { short: Some(b'1'), long: "oneline", takes_value: TakesValue::Forbidden };
|
||||
pub static LONG: Arg = Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden };
|
||||
pub static GRID: Arg = Arg { short: Some(b'G'), long: "grid", takes_value: TakesValue::Forbidden };
|
||||
pub static ACROSS: Arg = Arg { short: Some(b'x'), long: "across", takes_value: TakesValue::Forbidden };
|
||||
pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_value: TakesValue::Forbidden };
|
||||
pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", takes_value: TakesValue::Forbidden };
|
||||
pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden };
|
||||
|
||||
pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary(Some(COLOURS)) };
|
||||
pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary(Some(COLOURS)) };
|
||||
const COLOURS: &[&str] = &["always", "auto", "never"];
|
||||
|
||||
pub static COLOR_SCALE: Arg = Arg { short: None, long: "color-scale", takes_value: TakesValue::Forbidden };
|
||||
pub static COLOUR_SCALE: Arg = Arg { short: None, long: "colour-scale", takes_value: TakesValue::Forbidden };
|
||||
|
||||
// filtering and sorting options
|
||||
pub static ALL: Arg = Arg { short: Some(b'a'), long: "all", takes_value: TakesValue::Forbidden };
|
||||
pub static LIST_DIRS: Arg = Arg { short: Some(b'd'), long: "list-dirs", takes_value: TakesValue::Forbidden };
|
||||
pub static LEVEL: Arg = Arg { short: Some(b'L'), long: "level", takes_value: TakesValue::Necessary(None) };
|
||||
pub static REVERSE: Arg = Arg { short: Some(b'r'), long: "reverse", takes_value: TakesValue::Forbidden };
|
||||
pub static SORT: Arg = Arg { short: Some(b's'), long: "sort", takes_value: TakesValue::Necessary(Some(SORTS)) };
|
||||
pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary(None) };
|
||||
pub static GIT_IGNORE: Arg = Arg { short: None, long: "git-ignore", takes_value: TakesValue::Forbidden };
|
||||
pub static DIRS_FIRST: Arg = Arg { short: None, long: "group-directories-first", takes_value: TakesValue::Forbidden };
|
||||
pub static ONLY_DIRS: Arg = Arg { short: Some(b'D'), long: "only-dirs", takes_value: TakesValue::Forbidden };
|
||||
const SORTS: Values = &[ "name", "Name", "size", "extension",
|
||||
"Extension", "modified", "changed", "accessed",
|
||||
"created", "inode", "type", "none" ];
|
||||
|
||||
// display options
|
||||
pub static BINARY: Arg = Arg { short: Some(b'b'), long: "binary", takes_value: TakesValue::Forbidden };
|
||||
pub static BYTES: Arg = Arg { short: Some(b'B'), long: "bytes", takes_value: TakesValue::Forbidden };
|
||||
pub static GROUP: Arg = Arg { short: Some(b'g'), long: "group", takes_value: TakesValue::Forbidden };
|
||||
pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_value: TakesValue::Forbidden };
|
||||
pub static ICONS: Arg = Arg { short: None, long: "icons", takes_value: TakesValue::Forbidden };
|
||||
pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden };
|
||||
pub static LINKS: Arg = Arg { short: Some(b'H'), long: "links", takes_value: TakesValue::Forbidden };
|
||||
pub static MODIFIED: Arg = Arg { short: Some(b'm'), long: "modified", takes_value: TakesValue::Forbidden };
|
||||
pub static CHANGED: Arg = Arg { short: None, long: "changed", takes_value: TakesValue::Forbidden };
|
||||
pub static BLOCKS: Arg = Arg { short: Some(b'S'), long: "blocks", takes_value: TakesValue::Forbidden };
|
||||
pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Necessary(Some(TIMES)) };
|
||||
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)) };
|
||||
const TIMES: Values = &["modified", "changed", "accessed", "created"];
|
||||
const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso"];
|
||||
|
||||
// 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 ALL_ARGS: Args = Args(&[
|
||||
&VERSION, &HELP,
|
||||
|
||||
&ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY,
|
||||
&COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE,
|
||||
|
||||
&ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST,
|
||||
&IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS,
|
||||
|
||||
&BINARY, &BYTES, &GROUP, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED,
|
||||
&BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE,
|
||||
|
||||
&GIT, &EXTENDED,
|
||||
]);
|
@ -1,5 +1,9 @@
|
||||
use std::fmt;
|
||||
|
||||
use options::flags;
|
||||
use options::parser::MatchedFlags;
|
||||
use fs::feature::xattr;
|
||||
|
||||
|
||||
static OPTIONS: &str = r##"
|
||||
-?, --help show list of command-line options
|
||||
@ -15,16 +19,21 @@ DISPLAY OPTIONS
|
||||
-F, --classify display type indicator by file names
|
||||
--colo[u]r=WHEN when to use terminal colours (always, auto, never)
|
||||
--colo[u]r-scale highlight levels of file sizes distinctly
|
||||
--icons display icons
|
||||
|
||||
FILTERING AND SORTING OPTIONS
|
||||
-a, --all show hidden and 'dot' files
|
||||
-d, --list-dirs list directories like regular files
|
||||
-L, --level DEPTH limit the depth of recursion
|
||||
-r, --reverse reverse the sort order
|
||||
-s, --sort SORT_FIELD which field to sort by:
|
||||
-s, --sort SORT_FIELD which field to sort by
|
||||
--group-directories-first list directories before other files
|
||||
-D, --only-dirs list only directories
|
||||
-I, --ignore-glob GLOBS glob patterns (pipe-separated) of files to ignore
|
||||
--git-ignore Ignore files mentioned in '.gitignore'
|
||||
Valid sort fields: name, Name, extension, Extension, size, type,
|
||||
modified, accessed, created, inode, none
|
||||
modified, accessed, created, inode, and none.
|
||||
date, time, old, and new all refer to modified.
|
||||
"##;
|
||||
|
||||
static LONG_OPTIONS: &str = r##"
|
||||
@ -35,7 +44,6 @@ LONG VIEW OPTIONS
|
||||
-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
|
||||
-L, --level DEPTH limit the depth of recursion
|
||||
-m, --modified use the modified timestamp field
|
||||
-S, --blocks show number of file system blocks
|
||||
-t, --time FIELD which timestamp field to list (modified, accessed, created)
|
||||
@ -43,34 +51,104 @@ LONG VIEW OPTIONS
|
||||
-U, --created use the created timestamp field
|
||||
--time-style how to format timestamps (default, iso, long-iso, full-iso)"##;
|
||||
|
||||
static GIT_HELP: &str = r##" --git list each file's Git status, if tracked"##;
|
||||
static GIT_HELP: &str = r##" --git list each file's Git status, if tracked or ignored"##;
|
||||
static EXTENDED_HELP: &str = r##" -@, --extended list each file's extended attributes and sizes"##;
|
||||
|
||||
|
||||
/// 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)]
|
||||
pub struct HelpString {
|
||||
pub only_long: bool,
|
||||
pub git: bool,
|
||||
pub xattrs: bool,
|
||||
|
||||
/// Only show the help for the long section, not all the help.
|
||||
only_long: bool,
|
||||
|
||||
/// Whether the --git option should be included in the help.
|
||||
git: bool,
|
||||
|
||||
/// Whether the --extended option should be included in the help.
|
||||
xattrs: bool,
|
||||
}
|
||||
|
||||
impl HelpString {
|
||||
|
||||
/// Determines how to show help, if at all, based on the user’s
|
||||
/// command-line arguments. This one works backwards from the other
|
||||
/// ‘deduce’ functions, returning Err if help needs to be shown.
|
||||
///
|
||||
/// We don’t do any strict-mode error checking here: it’s OK to give
|
||||
/// the --help or --long flags more than once. Actually checking for
|
||||
/// errors when the user wants help is kind of petty!
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<(), HelpString> {
|
||||
if matches.count(&flags::HELP) > 0 {
|
||||
let only_long = matches.count(&flags::LONG) > 0;
|
||||
let git = cfg!(feature="git");
|
||||
let xattrs = xattr::ENABLED;
|
||||
Err(HelpString { only_long, git, xattrs })
|
||||
}
|
||||
else {
|
||||
Ok(()) // no help needs to be shown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HelpString {
|
||||
|
||||
/// Format this help options into an actual string of help
|
||||
/// text to be displayed to the user.
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
try!(write!(f, "Usage:\n exa [options] [files...]\n"));
|
||||
writeln!(f, "Usage:\n exa [options] [files...]")?;
|
||||
|
||||
if !self.only_long {
|
||||
try!(write!(f, "{}", OPTIONS));
|
||||
write!(f, "{}", OPTIONS)?;
|
||||
}
|
||||
|
||||
try!(write!(f, "{}", LONG_OPTIONS));
|
||||
write!(f, "{}", LONG_OPTIONS)?;
|
||||
|
||||
if self.git {
|
||||
try!(write!(f, "\n{}", GIT_HELP));
|
||||
write!(f, "\n{}", GIT_HELP)?;
|
||||
}
|
||||
|
||||
if self.xattrs {
|
||||
try!(write!(f, "\n{}", EXTENDED_HELP));
|
||||
write!(f, "\n{}", EXTENDED_HELP)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use options::Options;
|
||||
use std::ffi::OsString;
|
||||
|
||||
fn os(input: &'static str) -> OsString {
|
||||
let mut os = OsString::new();
|
||||
os.push(input);
|
||||
os
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
let args = [ os("--help") ];
|
||||
let opts = Options::parse(&args, &None);
|
||||
assert!(opts.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_with_file() {
|
||||
let args = [ os("--help"), os("me") ];
|
||||
let opts = Options::parse(&args, &None);
|
||||
assert!(opts.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unhelpful() {
|
||||
let args = [];
|
||||
let opts = Options::parse(&args, &None);
|
||||
assert!(opts.is_ok()) // no help when --help isn’t passed
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +1,50 @@
|
||||
use std::ffi::OsString;
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use getopts;
|
||||
use glob;
|
||||
|
||||
use options::help::HelpString;
|
||||
use options::{flags, HelpString, VersionString};
|
||||
use options::parser::{Arg, Flag, ParseError};
|
||||
|
||||
|
||||
/// A list of legal choices for an argument-taking option
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Choices(&'static [&'static str]);
|
||||
|
||||
impl fmt::Display for Choices {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "(choices: {})", self.0.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
/// A **misfire** is a thing that can happen instead of listing files -- a
|
||||
/// catch-all for anything outside the program’s normal execution.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum Misfire {
|
||||
|
||||
/// The getopts crate didn’t like these arguments.
|
||||
InvalidOptions(getopts::Fail),
|
||||
/// The getopts crate didn’t like these Arguments.
|
||||
InvalidOptions(ParseError),
|
||||
|
||||
/// The user supplied an illegal choice to an argument
|
||||
BadArgument(getopts::Fail, Choices),
|
||||
/// The user supplied an illegal choice to an Argument.
|
||||
BadArgument(&'static Arg, OsString),
|
||||
|
||||
/// The user supplied a set of options
|
||||
Unsupported(String),
|
||||
|
||||
/// The user asked for help. This isn’t strictly an error, which is why
|
||||
/// this enum isn’t named Error!
|
||||
Help(HelpString),
|
||||
|
||||
/// The user wanted the version number.
|
||||
Version,
|
||||
Version(VersionString),
|
||||
|
||||
/// An option was given twice or more in strict mode.
|
||||
Duplicate(Flag, Flag),
|
||||
|
||||
/// Two options were given that conflict with one another.
|
||||
Conflict(&'static str, &'static str),
|
||||
Conflict(&'static Arg, &'static Arg),
|
||||
|
||||
/// An option was given that does nothing when another one either is or
|
||||
/// isn't present.
|
||||
Useless(&'static str, bool, &'static str),
|
||||
Useless(&'static Arg, bool, &'static Arg),
|
||||
|
||||
/// An option was given that does nothing when either of two other options
|
||||
/// are not present.
|
||||
Useless2(&'static str, &'static str, &'static str),
|
||||
Useless2(&'static Arg, &'static Arg, &'static Arg),
|
||||
|
||||
/// A very specific edge case where --tree can’t be used with --all twice.
|
||||
TreeAllAll,
|
||||
|
||||
/// A numeric option was given that failed to be parsed as a number.
|
||||
FailedParse(ParseIntError),
|
||||
@ -58,21 +58,11 @@ impl Misfire {
|
||||
/// The OS return code this misfire should signify.
|
||||
pub fn is_error(&self) -> bool {
|
||||
match *self {
|
||||
Misfire::Help(_) => false,
|
||||
Misfire::Version => false,
|
||||
_ => true,
|
||||
Misfire::Help(_) => false,
|
||||
Misfire::Version(_) => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The Misfire that happens when an option gets given the wrong
|
||||
/// argument. This has to use one of the `getopts` failure
|
||||
/// variants--it’s meant to take just an option name, rather than an
|
||||
/// option *and* an argument, but it works just as well.
|
||||
pub fn bad_argument(option: &str, otherwise: &str, legal: &'static [&'static str]) -> Misfire {
|
||||
Misfire::BadArgument(getopts::Fail::UnrecognizedOption(format!(
|
||||
"--{} {}",
|
||||
option, otherwise)), Choices(legal))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<glob::PatternError> for Misfire {
|
||||
@ -83,19 +73,71 @@ impl From<glob::PatternError> for Misfire {
|
||||
|
||||
impl fmt::Display for Misfire {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use options::parser::TakesValue;
|
||||
use self::Misfire::*;
|
||||
|
||||
match *self {
|
||||
InvalidOptions(ref e) => write!(f, "{}", e),
|
||||
BadArgument(ref e, ref c) => write!(f, "{} {}", e, c),
|
||||
Help(ref text) => write!(f, "{}", text),
|
||||
Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")),
|
||||
Conflict(a, b) => write!(f, "Option --{} conflicts with option {}.", a, b),
|
||||
Useless(a, false, b) => write!(f, "Option --{} is useless without option --{}.", a, b),
|
||||
Useless(a, true, b) => write!(f, "Option --{} is useless given option --{}.", a, b),
|
||||
Useless2(a, b1, b2) => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2),
|
||||
FailedParse(ref e) => write!(f, "Failed to parse number: {}", e),
|
||||
FailedGlobPattern(ref e) => write!(f, "Failed to parse glob pattern: {}", e),
|
||||
BadArgument(ref arg, ref attempt) => {
|
||||
if let TakesValue::Necessary(Some(values)) = arg.takes_value {
|
||||
write!(f, "Option {} has no {:?} setting ({})", arg, attempt, Choices(values))
|
||||
}
|
||||
else {
|
||||
write!(f, "Option {} has no {:?} setting", arg, attempt)
|
||||
}
|
||||
},
|
||||
InvalidOptions(ref e) => write!(f, "{}", e),
|
||||
Unsupported(ref e) => write!(f, "{}", e),
|
||||
Help(ref text) => write!(f, "{}", text),
|
||||
Version(ref version) => write!(f, "{}", version),
|
||||
Conflict(ref a, ref b) => write!(f, "Option {} conflicts with option {}", a, b),
|
||||
Duplicate(ref a, ref b) => if a == b { write!(f, "Flag {} was given twice", a) }
|
||||
else { write!(f, "Flag {} conflicts with flag {}", a, b) },
|
||||
Useless(ref a, false, ref b) => write!(f, "Option {} is useless without option {}", a, b),
|
||||
Useless(ref a, true, ref b) => write!(f, "Option {} is useless given option {}", a, b),
|
||||
Useless2(ref a, ref b1, ref b2) => write!(f, "Option {} is useless without options {} or {}", a, b1, b2),
|
||||
TreeAllAll => write!(f, "Option --tree is useless given --all --all"),
|
||||
FailedParse(ref e) => write!(f, "Failed to parse number: {}", e),
|
||||
FailedGlobPattern(ref e) => write!(f, "Failed to parse glob pattern: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::ParseError::*;
|
||||
|
||||
match *self {
|
||||
NeedsValue { ref flag, values: None } => write!(f, "Flag {} needs a value", flag),
|
||||
NeedsValue { ref flag, values: Some(cs) } => write!(f, "Flag {} needs a value ({})", flag, Choices(cs)),
|
||||
ForbiddenValue { ref flag } => write!(f, "Flag {} cannot take a value", flag),
|
||||
UnknownShortArgument { ref attempt } => write!(f, "Unknown argument -{}", *attempt as char),
|
||||
UnknownArgument { ref attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Misfire {
|
||||
/// Try to second-guess what the user was trying to do, depending on what
|
||||
/// went wrong.
|
||||
pub fn suggestion(&self) -> Option<&'static str> {
|
||||
// ‘ls -lt’ and ‘ls -ltr’ are common combinations
|
||||
match *self {
|
||||
Misfire::BadArgument(ref time, ref r) if *time == &flags::TIME && r == "r" =>
|
||||
Some("To sort oldest files last, try \"--sort oldest\", or just \"-sold\""),
|
||||
Misfire::InvalidOptions(ParseError::NeedsValue { ref flag, .. }) if *flag == Flag::Short(b't') =>
|
||||
Some("To sort newest files last, try \"--sort newest\", or just \"-snew\""),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A list of legal choices for an argument-taking option.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Choices(&'static [&'static str]);
|
||||
|
||||
impl fmt::Display for Choices {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "choices: {}", self.0.join(", "))
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,100 @@
|
||||
use std::ffi::OsStr;
|
||||
//! Parsing command-line strings into exa options.
|
||||
//!
|
||||
//! This module imports exa’s configuration types, such as `View` (the details
|
||||
//! of displaying multiple files) and `DirAction` (what to do when encountering
|
||||
//! a directory), and implements `deduce` methods on them so they can be
|
||||
//! configured using command-line options.
|
||||
//!
|
||||
//!
|
||||
//! ## Useless and overridden options
|
||||
//!
|
||||
//! Let’s say exa was invoked with just one argument: `exa --inode`. The
|
||||
//! `--inode` option is used in the details view, where it adds the inode
|
||||
//! column to the output. But because the details view is *only* activated with
|
||||
//! the `--long` argument, adding `--inode` without it would not have any
|
||||
//! effect.
|
||||
//!
|
||||
//! For a long time, exa’s philosophy was that the user should be warned
|
||||
//! whenever they could be mistaken like this. If you tell exa to display the
|
||||
//! inode, and it *doesn’t* display the inode, isn’t that more annoying than
|
||||
//! having it throw an error back at you?
|
||||
//!
|
||||
//! However, this doesn’t take into account *configuration*. Say a user wants
|
||||
//! to configure exa so that it lists inodes in the details view, but otherwise
|
||||
//! functions normally. A common way to do this for command-line programs is to
|
||||
//! define a shell alias that specifies the details they want to use every
|
||||
//! time. For the inode column, the alias would be:
|
||||
//!
|
||||
//! `alias exa="exa --inode"`
|
||||
//!
|
||||
//! Using this alias means that although the inode column will be shown in the
|
||||
//! details view, you’re now *only* allowed to use the details view, as any
|
||||
//! other view type will result in an error. Oops!
|
||||
//!
|
||||
//! Another example is when an option is specified twice, such as `exa
|
||||
//! --sort=Name --sort=size`. Did the user change their mind about sorting, and
|
||||
//! accidentally specify the option twice?
|
||||
//!
|
||||
//! Again, exa rejected this case, throwing an error back to the user instead
|
||||
//! of trying to guess how they want their output sorted. And again, this
|
||||
//! doesn’t take into account aliases being used to set defaults. A user who
|
||||
//! wants their files to be sorted case-insensitively may configure their shell
|
||||
//! with the following:
|
||||
//!
|
||||
//! `alias exa="exa --sort=Name"`
|
||||
//!
|
||||
//! Just like the earlier example, the user now can’t use any other sort order,
|
||||
//! because exa refuses to guess which one they meant. It’s *more* annoying to
|
||||
//! have to go back and edit the command than if there were no error.
|
||||
//!
|
||||
//! Fortunately, there’s a heuristic for telling which options came from an
|
||||
//! alias and which came from the actual command-line: aliased options are
|
||||
//! nearer the beginning of the options array, and command-line options are
|
||||
//! nearer the end. This means that after the options have been parsed, exa
|
||||
//! needs to traverse them *backwards* to find the last-most-specified one.
|
||||
//!
|
||||
//! For example, invoking exa with `exa --sort=size` when that alias is present
|
||||
//! would result in a full command-line of:
|
||||
//!
|
||||
//! `exa --sort=Name --sort=size`
|
||||
//!
|
||||
//! `--sort=size` should override `--sort=Name` because it’s closer to the end
|
||||
//! of the arguments array. In fact, because there’s no way to tell where the
|
||||
//! arguments came from -- it’s just a heuristic -- this will still work even
|
||||
//! if no aliases are being used!
|
||||
//!
|
||||
//! Finally, this isn’t just useful when options could override each other.
|
||||
//! Creating an alias `exal="exa --long --inode --header"` then invoking `exal
|
||||
//! --grid --long` shouldn’t complain about `--long` being given twice when
|
||||
//! it’s clear what the user wants.
|
||||
|
||||
use getopts;
|
||||
|
||||
use fs::feature::xattr;
|
||||
use output::details;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
use fs::dir_action::DirAction;
|
||||
use fs::filter::FileFilter;
|
||||
use output::{View, Mode, details, grid_details};
|
||||
|
||||
mod style;
|
||||
mod dir_action;
|
||||
pub use self::dir_action::{DirAction, RecurseOptions};
|
||||
|
||||
mod filter;
|
||||
pub use self::filter::{FileFilter, SortField, SortCase};
|
||||
mod view;
|
||||
|
||||
mod help;
|
||||
use self::help::HelpString;
|
||||
|
||||
mod version;
|
||||
use self::version::VersionString;
|
||||
|
||||
mod misfire;
|
||||
pub use self::misfire::Misfire;
|
||||
|
||||
mod view;
|
||||
pub use self::view::{View, Mode};
|
||||
pub mod vars;
|
||||
pub use self::vars::Vars;
|
||||
|
||||
mod parser;
|
||||
mod flags;
|
||||
use self::parser::MatchedFlags;
|
||||
|
||||
|
||||
/// These **options** represent a parsed, error-checked versions of the
|
||||
@ -39,85 +115,31 @@ pub struct Options {
|
||||
|
||||
impl Options {
|
||||
|
||||
// Even though the arguments go in as OsStrings, they come out
|
||||
// as Strings. Invalid UTF-8 won’t be parsed, but it won’t make
|
||||
// exa core dump either.
|
||||
//
|
||||
// https://github.com/rust-lang-nursery/getopts/pull/29
|
||||
|
||||
/// Call getopts on the given slice of command-line strings.
|
||||
/// Parse the given iterator of command-line strings into an Options
|
||||
/// struct and a list of free filenames, using the environment variables
|
||||
/// for extra options.
|
||||
#[allow(unused_results)]
|
||||
pub fn getopts<C>(args: C) -> Result<(Options, Vec<String>), Misfire>
|
||||
where C: IntoIterator, C::Item: AsRef<OsStr> {
|
||||
let mut opts = getopts::Options::new();
|
||||
pub fn parse<'args, I, V>(args: I, vars: &V) -> Result<(Options, Vec<&'args OsStr>), Misfire>
|
||||
where I: IntoIterator<Item=&'args OsString>,
|
||||
V: Vars {
|
||||
use options::parser::{Matches, Strictness};
|
||||
|
||||
opts.optflag("v", "version", "show version of exa");
|
||||
opts.optflag("?", "help", "show list of command-line options");
|
||||
let strictness = match vars.get(vars::EXA_STRICT) {
|
||||
None => Strictness::UseLastArguments,
|
||||
Some(ref t) if t.is_empty() => Strictness::UseLastArguments,
|
||||
_ => Strictness::ComplainAboutRedundantArguments,
|
||||
};
|
||||
|
||||
// Display options
|
||||
opts.optflag("1", "oneline", "display one entry per line");
|
||||
opts.optflag("l", "long", "display extended file metadata in a table");
|
||||
opts.optflag("G", "grid", "display entries as a grid (default)");
|
||||
opts.optflag("x", "across", "sort the grid across, rather than downwards");
|
||||
opts.optflag("R", "recurse", "recurse into directories");
|
||||
opts.optflag("T", "tree", "recurse into directories as a tree");
|
||||
opts.optflag("F", "classify", "display type indicator by file names (one of */=@|)");
|
||||
opts.optopt ("", "color", "when to use terminal colours", "WHEN");
|
||||
opts.optopt ("", "colour", "when to use terminal colours", "WHEN");
|
||||
opts.optflag("", "color-scale", "highlight levels of file sizes distinctly");
|
||||
opts.optflag("", "colour-scale", "highlight levels of file sizes distinctly");
|
||||
|
||||
// Filtering and sorting options
|
||||
opts.optflag("", "group-directories-first", "sort directories before other files");
|
||||
opts.optflagmulti("a", "all", "show hidden and 'dot' files");
|
||||
opts.optflag("d", "list-dirs", "list directories like regular files");
|
||||
opts.optopt ("L", "level", "limit the depth of recursion", "DEPTH");
|
||||
opts.optflag("r", "reverse", "reverse the sert order");
|
||||
opts.optopt ("s", "sort", "which field to sort by", "WORD");
|
||||
opts.optopt ("I", "ignore-glob", "ignore files that match these glob patterns", "GLOB1|GLOB2...");
|
||||
|
||||
// Long view options
|
||||
opts.optflag("b", "binary", "list file sizes with binary prefixes");
|
||||
opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes");
|
||||
opts.optflag("g", "group", "list each file's group");
|
||||
opts.optflag("h", "header", "add a header row to each column");
|
||||
opts.optflag("H", "links", "list each file's number of hard links");
|
||||
opts.optflag("i", "inode", "list each file's inode number");
|
||||
opts.optflag("m", "modified", "use the modified timestamp field");
|
||||
opts.optflag("S", "blocks", "list each file's number of file system blocks");
|
||||
opts.optopt ("t", "time", "which timestamp field to show", "WORD");
|
||||
opts.optflag("u", "accessed", "use the accessed timestamp field");
|
||||
opts.optflag("U", "created", "use the created timestamp field");
|
||||
opts.optopt ("", "time-style", "how to format timestamp fields", "STYLE");
|
||||
|
||||
if cfg!(feature="git") {
|
||||
opts.optflag("", "git", "list each file's git status");
|
||||
}
|
||||
|
||||
if xattr::ENABLED {
|
||||
opts.optflag("@", "extended", "list each file's extended attribute keys and sizes");
|
||||
}
|
||||
|
||||
let matches = match opts.parse(args) {
|
||||
let Matches { flags, frees } = match flags::ALL_ARGS.parse(args, strictness) {
|
||||
Ok(m) => m,
|
||||
Err(e) => return Err(Misfire::InvalidOptions(e)),
|
||||
};
|
||||
|
||||
if matches.opt_present("help") {
|
||||
let help = HelpString {
|
||||
only_long: matches.opt_present("long"),
|
||||
git: cfg!(feature="git"),
|
||||
xattrs: xattr::ENABLED,
|
||||
};
|
||||
HelpString::deduce(&flags).map_err(Misfire::Help)?;
|
||||
VersionString::deduce(&flags).map_err(Misfire::Version)?;
|
||||
|
||||
return Err(Misfire::Help(help));
|
||||
}
|
||||
else if matches.opt_present("version") {
|
||||
return Err(Misfire::Version);
|
||||
}
|
||||
|
||||
let options = Options::deduce(&matches)?;
|
||||
Ok((options, matches.free))
|
||||
let options = Options::deduce(&flags, vars)?;
|
||||
Ok((options, frees))
|
||||
}
|
||||
|
||||
/// Whether the View specified in this set of options includes a Git
|
||||
@ -126,182 +148,98 @@ impl Options {
|
||||
pub fn should_scan_for_git(&self) -> bool {
|
||||
match self.view.mode {
|
||||
Mode::Details(details::Options { table: Some(ref table), .. }) |
|
||||
Mode::GridDetails(_, details::Options { table: Some(ref table), .. }) => table.should_scan_for_git(),
|
||||
Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.extra_columns.git,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines the complete set of options based on the given command-line
|
||||
/// arguments, after they’ve been parsed.
|
||||
fn deduce(matches: &getopts::Matches) -> Result<Options, Misfire> {
|
||||
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Options, Misfire> {
|
||||
let dir_action = DirAction::deduce(matches)?;
|
||||
let filter = FileFilter::deduce(matches)?;
|
||||
let view = View::deduce(matches)?;
|
||||
let view = View::deduce(matches, vars)?;
|
||||
|
||||
Ok(Options { dir_action, view, filter })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Options, Misfire, SortField, SortCase};
|
||||
use fs::DotFilter;
|
||||
use fs::feature::xattr;
|
||||
pub mod test {
|
||||
use super::{Options, Misfire, flags};
|
||||
use options::parser::{Arg, MatchedFlags};
|
||||
use std::ffi::OsString;
|
||||
|
||||
fn is_helpful<T>(misfire: Result<T, Misfire>) -> bool {
|
||||
match misfire {
|
||||
Err(Misfire::Help(_)) => true,
|
||||
_ => false,
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum Strictnesses {
|
||||
Last,
|
||||
Complain,
|
||||
Both,
|
||||
}
|
||||
|
||||
/// This function gets used by the other testing modules.
|
||||
/// It can run with one or both strictness values: if told to run with
|
||||
/// 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.
|
||||
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 options::parser::{Args, Strictness};
|
||||
|
||||
let bits = inputs.into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
|
||||
let mut result = Vec::new();
|
||||
|
||||
if strictnesses == Last || strictnesses == Both {
|
||||
let results = Args(args).parse(bits.iter(), Strictness::UseLastArguments);
|
||||
result.push(get(&results.unwrap().flags));
|
||||
}
|
||||
|
||||
if strictnesses == Complain || strictnesses == Both {
|
||||
let results = Args(args).parse(bits.iter(), Strictness::ComplainAboutRedundantArguments);
|
||||
result.push(get(&results.unwrap().flags));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
let opts = Options::getopts(&[ "--help".to_string() ]);
|
||||
assert!(is_helpful(opts))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_with_file() {
|
||||
let opts = Options::getopts(&[ "--help".to_string(), "me".to_string() ]);
|
||||
assert!(is_helpful(opts))
|
||||
/// Creates an `OSStr` (used in tests)
|
||||
#[cfg(test)]
|
||||
fn os(input: &str) -> OsString {
|
||||
let mut os = OsString::new();
|
||||
os.push(input);
|
||||
os
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files() {
|
||||
let args = Options::getopts(&[ "this file".to_string(), "that file".to_string() ]).unwrap().1;
|
||||
assert_eq!(args, vec![ "this file".to_string(), "that file".to_string() ])
|
||||
let args = [ os("this file"), os("that file") ];
|
||||
let outs = Options::parse(&args, &None).unwrap().1;
|
||||
assert_eq!(outs, vec![ &os("this file"), &os("that file") ])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_args() {
|
||||
let nothing: Vec<String> = Vec::new();
|
||||
let args = Options::getopts(¬hing).unwrap().1;
|
||||
assert!(args.is_empty()); // Listing the `.` directory is done in main.rs
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_sizes() {
|
||||
let opts = Options::getopts(&[ "--long", "--binary", "--bytes" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Conflict("binary", "bytes"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_binary() {
|
||||
let opts = Options::getopts(&[ "--binary" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("binary", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_bytes() {
|
||||
let opts = Options::getopts(&[ "--bytes" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("bytes", false, "long"))
|
||||
let nothing: Vec<OsString> = Vec::new();
|
||||
let outs = Options::parse(¬hing, &None).unwrap().1;
|
||||
assert!(outs.is_empty()); // Listing the `.` directory is done in main.rs
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_across() {
|
||||
let opts = Options::getopts(&[ "--long", "--across" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long"))
|
||||
let args = [ os("--long"), os("--across") ];
|
||||
let opts = Options::parse(&args, &None);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::LONG))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oneline_across() {
|
||||
let opts = Options::getopts(&[ "--oneline", "--across" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_header() {
|
||||
let opts = Options::getopts(&[ "--header" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("header", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_group() {
|
||||
let opts = Options::getopts(&[ "--group" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("group", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_inode() {
|
||||
let opts = Options::getopts(&[ "--inode" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("inode", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_links() {
|
||||
let opts = Options::getopts(&[ "--links" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("links", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_blocks() {
|
||||
let opts = Options::getopts(&[ "--blocks" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("blocks", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_size() {
|
||||
let opts = Options::getopts(&[ "--sort=size" ]);
|
||||
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_name() {
|
||||
let opts = Options::getopts(&[ "--sort=name" ]);
|
||||
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Sensitive));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_name_lowercase() {
|
||||
let opts = Options::getopts(&[ "--sort=Name" ]);
|
||||
assert_eq!(opts.unwrap().0.filter.sort_field, SortField::Name(SortCase::Insensitive));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature="git")]
|
||||
fn just_git() {
|
||||
let opts = Options::getopts(&[ "--git" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("git", false, "long"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_without_long() {
|
||||
if xattr::ENABLED {
|
||||
let opts = Options::getopts(&[ "--extended" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("extended", false, "long"))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_without_recurse_or_tree() {
|
||||
let opts = Options::getopts(&[ "--level", "69105" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless2("level", "recurse", "tree"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_all_with_tree() {
|
||||
let opts = Options::getopts(&[ "--all", "--all", "--tree" ]);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless("all --all", true, "tree"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nowt() {
|
||||
let nothing: Vec<String> = Vec::new();
|
||||
let dots = Options::getopts(¬hing).unwrap().0.filter.dot_filter;
|
||||
assert_eq!(dots, DotFilter::JustFiles);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all() {
|
||||
let dots = Options::getopts(&[ "--all".to_string() ]).unwrap().0.filter.dot_filter;
|
||||
assert_eq!(dots, DotFilter::Dotfiles);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allall() {
|
||||
let dots = Options::getopts(&[ "-a".to_string(), "-a".to_string() ]).unwrap().0.filter.dot_filter;
|
||||
assert_eq!(dots, DotFilter::DotfilesAndDots);
|
||||
let args = [ os("--oneline"), os("--across") ];
|
||||
let opts = Options::parse(&args, &None);
|
||||
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::ONE_LINE))
|
||||
}
|
||||
}
|
||||
|
688
src/options/parser.rs
Normal file
688
src/options/parser.rs
Normal file
@ -0,0 +1,688 @@
|
||||
//! A general parser for command-line options.
|
||||
//!
|
||||
//! exa uses its own hand-rolled parser for command-line options. It supports
|
||||
//! the following syntax:
|
||||
//!
|
||||
//! - Long options: `--inode`, `--grid`
|
||||
//! - Long options with values: `--sort size`, `--level=4`
|
||||
//! - Short options: `-i`, `-G`
|
||||
//! - Short options with values: `-ssize`, `-L=4`
|
||||
//!
|
||||
//! These values can be mixed and matched: `exa -lssize --grid`. If you’ve used
|
||||
//! other command-line programs, then hopefully it’ll work much like them.
|
||||
//!
|
||||
//! Because exa already has its own files for the help text, shell completions,
|
||||
//! man page, and readme, so it can get away with having the options parser do
|
||||
//! very little: all it really needs to do is parse a slice of strings.
|
||||
//!
|
||||
//!
|
||||
//! ## UTF-8 and `OsStr`
|
||||
//!
|
||||
//! The parser uses `OsStr` as its string type. This is necessary for exa to
|
||||
//! list files that have invalid UTF-8 in their names: by treating file paths
|
||||
//! as bytes with no encoding, a file can be specified on the command-line and
|
||||
//! be looked up without having to be encoded into a `str` first.
|
||||
//!
|
||||
//! It also avoids the overhead of checking for invalid UTF-8 when parsing
|
||||
//! command-line options, as all the options and their values (such as
|
||||
//! `--sort size`) are guaranteed to just be 8-bit ASCII.
|
||||
|
||||
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::fmt;
|
||||
|
||||
use options::Misfire;
|
||||
|
||||
|
||||
/// A **short argument** is a single ASCII character.
|
||||
pub type ShortArg = u8;
|
||||
|
||||
/// A **long argument** is a string. This can be a UTF-8 string, even though
|
||||
/// the arguments will all be unchecked OsStrings, because we don’t actually
|
||||
/// store the user’s input after it’s been matched to a flag, we just store
|
||||
/// which flag it was.
|
||||
pub type LongArg = &'static str;
|
||||
|
||||
/// A **list of values** that an option can have, to be displayed when the
|
||||
/// user enters an invalid one or skips it.
|
||||
///
|
||||
/// This is literally just help text, and won’t be used to validate a value to
|
||||
/// see if it’s correct.
|
||||
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, Clone)]
|
||||
pub enum Flag {
|
||||
Short(ShortArg),
|
||||
Long(LongArg),
|
||||
}
|
||||
|
||||
impl Flag {
|
||||
pub fn matches(&self, arg: &Arg) -> bool {
|
||||
match *self {
|
||||
Flag::Short(short) => arg.short == Some(short),
|
||||
Flag::Long(long) => arg.long == long,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Flag {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Flag::Short(short) => write!(f, "-{}", short as char),
|
||||
Flag::Long(long) => write!(f, "--{}", long),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether redundant arguments should be considered a problem.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum Strictness {
|
||||
|
||||
/// Throw an error when an argument doesn’t do anything, either because
|
||||
/// it requires another argument to be specified, or because two conflict.
|
||||
ComplainAboutRedundantArguments,
|
||||
|
||||
/// Search the arguments list back-to-front, giving ones specified later
|
||||
/// in the list priority over earlier ones.
|
||||
UseLastArguments,
|
||||
}
|
||||
|
||||
/// Whether a flag takes a value. This is applicable to both long and short
|
||||
/// arguments.
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub enum TakesValue {
|
||||
|
||||
/// This flag has to be followed by a value.
|
||||
/// If there’s a fixed set of possible values, they can be printed out
|
||||
/// with the error text.
|
||||
Necessary(Option<Values>),
|
||||
|
||||
/// This flag will throw an error if there’s a value after it.
|
||||
Forbidden,
|
||||
}
|
||||
|
||||
|
||||
/// An **argument** can be matched by one of the user’s input strings.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Arg {
|
||||
|
||||
/// The short argument that matches it, if any.
|
||||
pub short: Option<ShortArg>,
|
||||
|
||||
/// The long argument that matches it. This is non-optional; all flags
|
||||
/// should at least have a descriptive long name.
|
||||
pub long: LongArg,
|
||||
|
||||
/// Whether this flag takes a value or not.
|
||||
pub takes_value: TakesValue,
|
||||
}
|
||||
|
||||
impl fmt::Display for Arg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
write!(f, "--{}", self.long)?;
|
||||
|
||||
if let Some(short) = self.short {
|
||||
write!(f, " (-{})", short as char)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Literally just several args.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Args(pub &'static [&'static Arg]);
|
||||
|
||||
impl Args {
|
||||
|
||||
/// Iterates over the given list of command-line arguments and parses
|
||||
/// them into a list of matched flags and free strings.
|
||||
pub fn parse<'args, I>(&self, inputs: I, strictness: Strictness) -> Result<Matches<'args>, ParseError>
|
||||
where I: IntoIterator<Item=&'args OsString> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use self::TakesValue::*;
|
||||
|
||||
let mut parsing = true;
|
||||
|
||||
// The results that get built up.
|
||||
let mut result_flags = Vec::new();
|
||||
let mut frees: Vec<&OsStr> = Vec::new();
|
||||
|
||||
// Iterate over the inputs with “while let” because we need to advance
|
||||
// the iterator manually whenever an argument that takes a value
|
||||
// doesn’t have one in its string so it needs the next one.
|
||||
let mut inputs = inputs.into_iter();
|
||||
while let Some(arg) = inputs.next() {
|
||||
let bytes = arg.as_bytes();
|
||||
|
||||
// Stop parsing if one of the arguments is the literal string “--”.
|
||||
// This allows a file named “--arg” to be specified by passing in
|
||||
// the pair “-- --arg”, without it getting matched as a flag that
|
||||
// doesn’t exist.
|
||||
if !parsing {
|
||||
frees.push(arg)
|
||||
}
|
||||
else if arg == "--" {
|
||||
parsing = false;
|
||||
}
|
||||
|
||||
// If the string starts with *two* dashes then it’s a long argument.
|
||||
else if bytes.starts_with(b"--") {
|
||||
let long_arg_name = OsStr::from_bytes(&bytes[2..]);
|
||||
|
||||
// If there’s an equals in it, then the string before the
|
||||
// equals will be the flag’s name, and the string after it
|
||||
// will be its value.
|
||||
if let Some((before, after)) = split_on_equals(long_arg_name) {
|
||||
let arg = self.lookup_long(before)?;
|
||||
let flag = Flag::Long(arg.long);
|
||||
match arg.takes_value {
|
||||
Necessary(_) => result_flags.push((flag, Some(after))),
|
||||
Forbidden => return Err(ParseError::ForbiddenValue { flag })
|
||||
}
|
||||
}
|
||||
|
||||
// If there’s no equals, then the entire string (apart from
|
||||
// the dashes) is the argument name.
|
||||
else {
|
||||
let arg = self.lookup_long(long_arg_name)?;
|
||||
let flag = Flag::Long(arg.long);
|
||||
match arg.takes_value {
|
||||
Forbidden => result_flags.push((flag, None)),
|
||||
Necessary(values) => {
|
||||
if let Some(next_arg) = inputs.next() {
|
||||
result_flags.push((flag, Some(next_arg)));
|
||||
}
|
||||
else {
|
||||
return Err(ParseError::NeedsValue { flag, values })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the string starts with *one* dash then it’s one or more
|
||||
// short arguments.
|
||||
else if bytes.starts_with(b"-") && arg != "-" {
|
||||
let short_arg = OsStr::from_bytes(&bytes[1..]);
|
||||
|
||||
// If there’s an equals in it, then the argument immediately
|
||||
// before the equals was the one that has the value, with the
|
||||
// others (if any) as value-less short ones.
|
||||
//
|
||||
// -x=abc => ‘x=abc’
|
||||
// -abcdx=fgh => ‘a’, ‘b’, ‘c’, ‘d’, ‘x=fgh’
|
||||
// -x= => error
|
||||
// -abcdx= => error
|
||||
//
|
||||
// There’s no way to give two values in a cluster like this:
|
||||
// it’s an error if any of the first set of arguments actually
|
||||
// takes a value.
|
||||
if let Some((before, after)) = split_on_equals(short_arg) {
|
||||
let (arg_with_value, other_args) = before.as_bytes().split_last().unwrap();
|
||||
|
||||
// Process the characters immediately following the dash...
|
||||
for byte in other_args {
|
||||
let arg = self.lookup_short(*byte)?;
|
||||
let flag = Flag::Short(*byte);
|
||||
match arg.takes_value {
|
||||
Forbidden => result_flags.push((flag, None)),
|
||||
Necessary(values) => return Err(ParseError::NeedsValue { flag, values })
|
||||
}
|
||||
}
|
||||
|
||||
// ...then the last one and the value after the equals.
|
||||
let arg = self.lookup_short(*arg_with_value)?;
|
||||
let flag = Flag::Short(arg.short.unwrap());
|
||||
match arg.takes_value {
|
||||
Necessary(_) => result_flags.push((flag, Some(after))),
|
||||
Forbidden => return Err(ParseError::ForbiddenValue { flag })
|
||||
}
|
||||
}
|
||||
|
||||
// If there’s no equals, then every character is parsed as
|
||||
// its own short argument. However, if any of the arguments
|
||||
// takes a value, then the *rest* of the string is used as
|
||||
// its value, and if there’s no rest of the string, then it
|
||||
// uses the next one in the iterator.
|
||||
//
|
||||
// -a => ‘a’
|
||||
// -abc => ‘a’, ‘b’, ‘c’
|
||||
// -abxdef => ‘a’, ‘b’, ‘x=def’
|
||||
// -abx def => ‘a’, ‘b’, ‘x=def’
|
||||
// -abx => error
|
||||
//
|
||||
else {
|
||||
for (index, byte) in bytes.into_iter().enumerate().skip(1) {
|
||||
let arg = self.lookup_short(*byte)?;
|
||||
let flag = Flag::Short(*byte);
|
||||
match arg.takes_value {
|
||||
Forbidden => result_flags.push((flag, None)),
|
||||
Necessary(values) => {
|
||||
if index < bytes.len() - 1 {
|
||||
let remnants = &bytes[index+1 ..];
|
||||
result_flags.push((flag, Some(OsStr::from_bytes(remnants))));
|
||||
break;
|
||||
}
|
||||
else if let Some(next_arg) = inputs.next() {
|
||||
result_flags.push((flag, Some(next_arg)));
|
||||
}
|
||||
else {
|
||||
return Err(ParseError::NeedsValue { flag, values })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, it’s a free string, usually a file name.
|
||||
else {
|
||||
frees.push(arg)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Matches { frees, flags: MatchedFlags { flags: result_flags, strictness } })
|
||||
}
|
||||
|
||||
fn lookup_short(&self, short: ShortArg) -> Result<&Arg, ParseError> {
|
||||
match self.0.into_iter().find(|arg| arg.short == Some(short)) {
|
||||
Some(arg) => Ok(arg),
|
||||
None => Err(ParseError::UnknownShortArgument { attempt: short })
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_long<'b>(&self, long: &'b OsStr) -> Result<&Arg, ParseError> {
|
||||
match self.0.into_iter().find(|arg| arg.long == long) {
|
||||
Some(arg) => Ok(arg),
|
||||
None => Err(ParseError::UnknownArgument { attempt: long.to_os_string() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The **matches** are the result of parsing the user’s command-line strings.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Matches<'args> {
|
||||
|
||||
/// The flags that were parsed from the user’s input.
|
||||
pub flags: MatchedFlags<'args>,
|
||||
|
||||
/// All the strings that weren’t matched as arguments, as well as anything
|
||||
/// after the special "--" string.
|
||||
pub frees: Vec<&'args OsStr>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct MatchedFlags<'args> {
|
||||
|
||||
/// The individual flags from the user’s input, in the order they were
|
||||
/// originally given.
|
||||
///
|
||||
/// Long and short arguments need to be kept in the same vector because
|
||||
/// we usually want the one nearest the end to count, and to know this,
|
||||
/// we need to know where they are in relation to one another.
|
||||
flags: Vec<(Flag, Option<&'args OsStr>)>,
|
||||
|
||||
/// Whether to check for duplicate or redundant arguments.
|
||||
strictness: Strictness,
|
||||
}
|
||||
|
||||
impl<'a> MatchedFlags<'a> {
|
||||
|
||||
/// Whether the given argument was specified.
|
||||
/// Returns `true` if it was, `false` if it wasn’t, and an error in
|
||||
/// strict mode if it was specified more than once.
|
||||
pub fn has(&self, arg: &'static Arg) -> Result<bool, Misfire> {
|
||||
self.has_where(|flag| flag.matches(arg)).map(|flag| flag.is_some())
|
||||
}
|
||||
|
||||
/// Returns the first found argument that satisfies the predicate, or
|
||||
/// nothing if none is found, or an error in strict mode if multiple
|
||||
/// argument satisfy the predicate.
|
||||
///
|
||||
/// You’ll have to test the resulting flag to see which argument it was.
|
||||
pub fn has_where<P>(&self, predicate: P) -> Result<Option<&Flag>, Misfire>
|
||||
where P: Fn(&Flag) -> bool {
|
||||
if self.is_strict() {
|
||||
let all = self.flags.iter()
|
||||
.filter(|tuple| tuple.1.is_none() && predicate(&tuple.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if all.len() < 2 { Ok(all.first().map(|t| &t.0)) }
|
||||
else { Err(Misfire::Duplicate(all[0].0.clone(), all[1].0.clone())) }
|
||||
}
|
||||
else {
|
||||
let any = self.flags.iter().rev()
|
||||
.find(|tuple| tuple.1.is_none() && predicate(&tuple.0))
|
||||
.map(|tuple| &tuple.0);
|
||||
Ok(any)
|
||||
}
|
||||
}
|
||||
|
||||
// This code could probably be better.
|
||||
// Both ‘has’ and ‘get’ immediately begin with a conditional, which makes
|
||||
// me think the functionality could be moved to inside Strictness.
|
||||
|
||||
/// Returns the value of the given argument if it was specified, nothing
|
||||
/// if it wasn’t, and an error in strict mode if it was specified more
|
||||
/// than once.
|
||||
pub fn get(&self, arg: &'static Arg) -> Result<Option<&OsStr>, Misfire> {
|
||||
self.get_where(|flag| flag.matches(arg))
|
||||
}
|
||||
|
||||
/// Returns the value of the argument that matches the predicate if it
|
||||
/// was specified, nothing if it wasn’t, and an error in strict mode if
|
||||
/// multiple arguments matched the predicate.
|
||||
///
|
||||
/// It’s not possible to tell which flag the value belonged to from this.
|
||||
pub fn get_where<P>(&self, predicate: P) -> Result<Option<&OsStr>, Misfire>
|
||||
where P: Fn(&Flag) -> bool {
|
||||
if self.is_strict() {
|
||||
let those = self.flags.iter()
|
||||
.filter(|tuple| tuple.1.is_some() && predicate(&tuple.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if those.len() < 2 { Ok(those.first().cloned().map(|t| t.1.unwrap())) }
|
||||
else { Err(Misfire::Duplicate(those[0].0.clone(), those[1].0.clone())) }
|
||||
}
|
||||
else {
|
||||
let found = self.flags.iter().rev()
|
||||
.find(|tuple| tuple.1.is_some() && predicate(&tuple.0))
|
||||
.map(|tuple| tuple.1.unwrap());
|
||||
Ok(found)
|
||||
}
|
||||
}
|
||||
|
||||
// It’s annoying that ‘has’ and ‘get’ won’t work when accidentally given
|
||||
// flags that do/don’t take values, but this should be caught by tests.
|
||||
|
||||
/// Counts the number of occurrences of the given argument, even in
|
||||
/// strict mode.
|
||||
pub fn count(&self, arg: &Arg) -> usize {
|
||||
self.flags.iter()
|
||||
.filter(|tuple| tuple.0.matches(arg))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Checks whether strict mode is on. This is usually done from within
|
||||
/// ‘has’ and ‘get’, but it’s available in an emergency.
|
||||
pub fn is_strict(&self) -> bool {
|
||||
self.strictness == Strictness::ComplainAboutRedundantArguments
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A problem with the user’s input that meant it couldn’t be parsed into a
|
||||
/// coherent list of arguments.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum ParseError {
|
||||
|
||||
/// A flag that has to take a value was not given one.
|
||||
NeedsValue { flag: Flag, values: Option<Values> },
|
||||
|
||||
/// A flag that can’t take a value *was* given one.
|
||||
ForbiddenValue { flag: Flag },
|
||||
|
||||
/// A short argument, either alone or in a cluster, was not
|
||||
/// recognised by the program.
|
||||
UnknownShortArgument { attempt: ShortArg },
|
||||
|
||||
/// A long argument was not recognised by the program.
|
||||
/// We don’t have a known &str version of the flag, so
|
||||
/// this may not be valid UTF-8.
|
||||
UnknownArgument { attempt: OsString },
|
||||
}
|
||||
|
||||
// It’s technically possible for ParseError::UnknownArgument to borrow its
|
||||
// OsStr rather than owning it, but that would give ParseError a lifetime,
|
||||
// which would give Misfire a lifetime, which gets used everywhere. And this
|
||||
// only happens when an error occurs, so it’s not really worth it.
|
||||
|
||||
|
||||
/// Splits a string on its `=` character, returning the two substrings on
|
||||
/// either side. Returns `None` if there’s no equals or a string is missing.
|
||||
fn split_on_equals(input: &OsStr) -> Option<(&OsStr, &OsStr)> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
if let Some(index) = input.as_bytes().iter().position(|elem| *elem == b'=') {
|
||||
let (before, after) = input.as_bytes().split_at(index);
|
||||
|
||||
// The after string contains the = that we need to remove.
|
||||
if !before.is_empty() && after.len() >= 2 {
|
||||
return Some((OsStr::from_bytes(before),
|
||||
OsStr::from_bytes(&after[1..])))
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
/// Creates an `OSString` (used in tests)
|
||||
#[cfg(test)]
|
||||
fn os(input: &'static str) -> OsString {
|
||||
let mut os = OsString::new();
|
||||
os.push(input);
|
||||
os
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod split_test {
|
||||
use super::{split_on_equals, os};
|
||||
|
||||
macro_rules! test_split {
|
||||
($name:ident: $input:expr => None) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
assert_eq!(split_on_equals(&os($input)),
|
||||
None);
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $input:expr => $before:expr, $after:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
assert_eq!(split_on_equals(&os($input)),
|
||||
Some((&*os($before), &*os($after))));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_split!(empty: "" => None);
|
||||
test_split!(letter: "a" => None);
|
||||
|
||||
test_split!(just: "=" => None);
|
||||
test_split!(intro: "=bbb" => None);
|
||||
test_split!(denou: "aaa=" => None);
|
||||
test_split!(equals: "aaa=bbb" => "aaa", "bbb");
|
||||
|
||||
test_split!(sort: "--sort=size" => "--sort", "size");
|
||||
test_split!(more: "this=that=other" => "this", "that=other");
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod parse_test {
|
||||
use super::*;
|
||||
|
||||
pub fn os(input: &'static str) -> OsString {
|
||||
let mut os = OsString::new();
|
||||
os.push(input);
|
||||
os
|
||||
}
|
||||
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $inputs:expr => frees: $frees:expr, flags: $flags:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
|
||||
// Annoyingly the input &strs need to be converted to OsStrings
|
||||
let inputs: Vec<OsString> = $inputs.as_ref().into_iter().map(|&o| os(o)).collect();
|
||||
|
||||
// Same with the frees
|
||||
let frees: Vec<OsString> = $frees.as_ref().into_iter().map(|&o| os(o)).collect();
|
||||
let frees: Vec<&OsStr> = frees.iter().map(|os| os.as_os_str()).collect();
|
||||
|
||||
let flags = <[_]>::into_vec(Box::new($flags));
|
||||
|
||||
let strictness = Strictness::UseLastArguments; // this isn’t even used
|
||||
let got = Args(TEST_ARGS).parse(inputs.iter(), strictness);
|
||||
let expected = Ok(Matches { frees, flags: MatchedFlags { flags, strictness } });
|
||||
assert_eq!(got, expected);
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $inputs:expr => error $error:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
use self::ParseError::*;
|
||||
|
||||
let strictness = Strictness::UseLastArguments; // this isn’t even used
|
||||
let bits = $inputs.as_ref().into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
|
||||
let got = Args(TEST_ARGS).parse(bits.iter(), strictness);
|
||||
|
||||
assert_eq!(got, Err($error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const SUGGESTIONS: Values = &[ "example" ];
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[
|
||||
&Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden },
|
||||
&Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden },
|
||||
&Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary(None) },
|
||||
&Arg { short: Some(b't'), long: "type", takes_value: TakesValue::Necessary(Some(SUGGESTIONS)) }
|
||||
];
|
||||
|
||||
|
||||
// Just filenames
|
||||
test!(empty: [] => frees: [], flags: []);
|
||||
test!(one_arg: ["exa"] => frees: [ "exa" ], flags: []);
|
||||
|
||||
// Dashes and double dashes
|
||||
test!(one_dash: ["-"] => frees: [ "-" ], flags: []);
|
||||
test!(two_dashes: ["--"] => frees: [], flags: []);
|
||||
test!(two_file: ["--", "file"] => frees: [ "file" ], flags: []);
|
||||
test!(two_arg_l: ["--", "--long"] => frees: [ "--long" ], flags: []);
|
||||
test!(two_arg_s: ["--", "-l"] => frees: [ "-l" ], flags: []);
|
||||
|
||||
|
||||
// Long args
|
||||
test!(long: ["--long"] => frees: [], flags: [ (Flag::Long("long"), None) ]);
|
||||
test!(long_then: ["--long", "4"] => frees: [ "4" ], flags: [ (Flag::Long("long"), None) ]);
|
||||
test!(long_two: ["--long", "--verbose"] => frees: [], flags: [ (Flag::Long("long"), None), (Flag::Long("verbose"), None) ]);
|
||||
|
||||
// Long args with values
|
||||
test!(bad_equals: ["--long=equals"] => error ForbiddenValue { flag: Flag::Long("long") });
|
||||
test!(no_arg: ["--count"] => error NeedsValue { flag: Flag::Long("count"), values: None });
|
||||
test!(arg_equals: ["--count=4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsStr::new("4"))) ]);
|
||||
test!(arg_then: ["--count", "4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsStr::new("4"))) ]);
|
||||
|
||||
// Long args with values and suggestions
|
||||
test!(no_arg_s: ["--type"] => error NeedsValue { flag: Flag::Long("type"), values: Some(SUGGESTIONS) });
|
||||
test!(arg_equals_s: ["--type=exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsStr::new("exa"))) ]);
|
||||
test!(arg_then_s: ["--type", "exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsStr::new("exa"))) ]);
|
||||
|
||||
|
||||
// Short args
|
||||
test!(short: ["-l"] => frees: [], flags: [ (Flag::Short(b'l'), None) ]);
|
||||
test!(short_then: ["-l", "4"] => frees: [ "4" ], flags: [ (Flag::Short(b'l'), None) ]);
|
||||
test!(short_two: ["-lv"] => frees: [], flags: [ (Flag::Short(b'l'), None), (Flag::Short(b'v'), None) ]);
|
||||
test!(mixed: ["-v", "--long"] => frees: [], flags: [ (Flag::Short(b'v'), None), (Flag::Long("long"), None) ]);
|
||||
|
||||
// Short args with values
|
||||
test!(bad_short: ["-l=equals"] => error ForbiddenValue { flag: Flag::Short(b'l') });
|
||||
test!(short_none: ["-c"] => error NeedsValue { flag: Flag::Short(b'c'), values: None });
|
||||
test!(short_arg_eq: ["-c=4"] => frees: [], flags: [(Flag::Short(b'c'), Some(OsStr::new("4"))) ]);
|
||||
test!(short_arg_then: ["-c", "4"] => frees: [], flags: [(Flag::Short(b'c'), Some(OsStr::new("4"))) ]);
|
||||
test!(short_two_together: ["-lctwo"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]);
|
||||
test!(short_two_equals: ["-lc=two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]);
|
||||
test!(short_two_next: ["-lc", "two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]);
|
||||
|
||||
// Short args with values and suggestions
|
||||
test!(short_none_s: ["-t"] => error NeedsValue { flag: Flag::Short(b't'), values: Some(SUGGESTIONS) });
|
||||
test!(short_two_together_s: ["-texa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]);
|
||||
test!(short_two_equals_s: ["-t=exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]);
|
||||
test!(short_two_next_s: ["-t", "exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]);
|
||||
|
||||
|
||||
// Unknown args
|
||||
test!(unknown_long: ["--quiet"] => error UnknownArgument { attempt: os("quiet") });
|
||||
test!(unknown_long_eq: ["--quiet=shhh"] => error UnknownArgument { attempt: os("quiet") });
|
||||
test!(unknown_short: ["-q"] => error UnknownShortArgument { attempt: b'q' });
|
||||
test!(unknown_short_2nd: ["-lq"] => error UnknownShortArgument { attempt: b'q' });
|
||||
test!(unknown_short_eq: ["-q=shhh"] => error UnknownShortArgument { attempt: b'q' });
|
||||
test!(unknown_short_2nd_eq: ["-lq=shhh"] => error UnknownShortArgument { attempt: b'q' });
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod matches_test {
|
||||
use super::*;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $input:expr, has $param:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let flags = MatchedFlags {
|
||||
flags: $input.to_vec(),
|
||||
strictness: Strictness::UseLastArguments,
|
||||
};
|
||||
|
||||
assert_eq!(flags.has(&$param), Ok($result));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static VERBOSE: Arg = Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden };
|
||||
static COUNT: Arg = Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary(None) };
|
||||
|
||||
|
||||
test!(short_never: [], has VERBOSE => false);
|
||||
test!(short_once: [(Flag::Short(b'v'), None)], has VERBOSE => true);
|
||||
test!(short_twice: [(Flag::Short(b'v'), None), (Flag::Short(b'v'), None)], has VERBOSE => true);
|
||||
test!(long_once: [(Flag::Long("verbose"), None)], has VERBOSE => true);
|
||||
test!(long_twice: [(Flag::Long("verbose"), None), (Flag::Long("verbose"), None)], has VERBOSE => true);
|
||||
test!(long_mixed: [(Flag::Long("verbose"), None), (Flag::Short(b'v'), None)], has VERBOSE => true);
|
||||
|
||||
|
||||
#[test]
|
||||
fn only_count() {
|
||||
let everything = os("everything");
|
||||
|
||||
let flags = MatchedFlags {
|
||||
flags: vec![ (Flag::Short(b'c'), Some(&*everything)) ],
|
||||
strictness: Strictness::UseLastArguments,
|
||||
};
|
||||
|
||||
assert_eq!(flags.get(&COUNT), Ok(Some(&*everything)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rightmost_count() {
|
||||
let everything = os("everything");
|
||||
let nothing = os("nothing");
|
||||
|
||||
let flags = MatchedFlags {
|
||||
flags: vec![ (Flag::Short(b'c'), Some(&*everything)),
|
||||
(Flag::Short(b'c'), Some(&*nothing)) ],
|
||||
strictness: Strictness::UseLastArguments,
|
||||
};
|
||||
|
||||
assert_eq!(flags.get(&COUNT), Ok(Some(&*nothing)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_count() {
|
||||
let flags = MatchedFlags { flags: Vec::new(), strictness: Strictness::UseLastArguments };
|
||||
|
||||
assert!(!flags.has(&COUNT).unwrap());
|
||||
}
|
||||
}
|
520
src/options/style.rs
Normal file
520
src/options/style.rs
Normal file
@ -0,0 +1,520 @@
|
||||
use ansi_term::Style;
|
||||
use glob;
|
||||
|
||||
use fs::File;
|
||||
use options::{flags, Vars, Misfire};
|
||||
use options::parser::MatchedFlags;
|
||||
use output::file_name::{FileStyle, Classify};
|
||||
use style::Colours;
|
||||
|
||||
|
||||
/// Under what circumstances we should display coloured, rather than plain,
|
||||
/// output to the terminal.
|
||||
///
|
||||
/// By default, we want to display the colours when stdout can display them.
|
||||
/// 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)]
|
||||
enum TerminalColours {
|
||||
|
||||
/// Display them even when output isn’t going to a terminal.
|
||||
Always,
|
||||
|
||||
/// Display them when output is going to a terminal, but not otherwise.
|
||||
Automatic,
|
||||
|
||||
/// Never display them, even when output is going to a terminal.
|
||||
Never,
|
||||
}
|
||||
|
||||
impl Default for TerminalColours {
|
||||
fn default() -> TerminalColours {
|
||||
TerminalColours::Automatic
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TerminalColours {
|
||||
|
||||
/// Determine which terminal colour conditions to use.
|
||||
fn deduce(matches: &MatchedFlags) -> Result<TerminalColours, Misfire> {
|
||||
|
||||
let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? {
|
||||
Some(w) => w,
|
||||
None => return Ok(TerminalColours::default()),
|
||||
};
|
||||
|
||||
if word == "always" {
|
||||
Ok(TerminalColours::Always)
|
||||
}
|
||||
else if word == "auto" || word == "automatic" {
|
||||
Ok(TerminalColours::Automatic)
|
||||
}
|
||||
else if word == "never" {
|
||||
Ok(TerminalColours::Never)
|
||||
}
|
||||
else {
|
||||
Err(Misfire::BadArgument(&flags::COLOR, word.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// **Styles**, which is already an overloaded term, is a pair of view option
|
||||
/// sets that happen to both be affected by `LS_COLORS` and `EXA_COLORS`.
|
||||
/// Because it’s better to only iterate through that once, the two are deduced
|
||||
/// together.
|
||||
pub struct Styles {
|
||||
|
||||
/// The colours to paint user interface elements, like the date column,
|
||||
/// and file kinds, such as directories.
|
||||
pub colours: Colours,
|
||||
|
||||
/// The colours to paint the names of files that match glob patterns
|
||||
/// (and the classify option).
|
||||
pub style: FileStyle,
|
||||
}
|
||||
|
||||
impl Styles {
|
||||
|
||||
#[allow(trivial_casts)] // the "as Box<_>" stuff below warns about this for some reason
|
||||
pub fn deduce<V, TW>(matches: &MatchedFlags, vars: &V, widther: TW) -> Result<Self, Misfire>
|
||||
where TW: Fn() -> Option<usize>, V: Vars {
|
||||
use self::TerminalColours::*;
|
||||
use info::filetype::FileExtensions;
|
||||
use output::file_name::NoFileColours;
|
||||
|
||||
let classify = Classify::deduce(matches)?;
|
||||
|
||||
// Before we do anything else, figure out if we need to consider
|
||||
// custom colours at all
|
||||
let tc = TerminalColours::deduce(matches)?;
|
||||
if tc == Never || (tc == Automatic && widther().is_none()) {
|
||||
return Ok(Styles {
|
||||
colours: Colours::plain(),
|
||||
style: FileStyle { classify, exts: Box::new(NoFileColours) },
|
||||
});
|
||||
}
|
||||
|
||||
// Parse the environment variables into colours and extension mappings
|
||||
let scale = matches.has_where(|f| f.matches(&flags::COLOR_SCALE) || f.matches(&flags::COLOUR_SCALE))?;
|
||||
let mut colours = Colours::colourful(scale.is_some());
|
||||
|
||||
let (exts, use_default_filetypes) = parse_color_vars(vars, &mut colours);
|
||||
|
||||
// Use between 0 and 2 file name highlighters
|
||||
let exts = match (exts.is_non_empty(), use_default_filetypes) {
|
||||
(false, false) => Box::new(NoFileColours) as Box<_>,
|
||||
(false, true) => Box::new(FileExtensions) as Box<_>,
|
||||
( true, false) => Box::new(exts) as Box<_>,
|
||||
( true, true) => Box::new((exts, FileExtensions)) as Box<_>,
|
||||
};
|
||||
|
||||
let style = FileStyle { classify, exts };
|
||||
Ok(Styles { colours, style })
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the environment variables into LS_COLORS pairs, putting file glob
|
||||
/// colours into the `ExtensionMappings` that gets returned, and using the
|
||||
/// two-character UI codes to modify the mutable `Colours`.
|
||||
///
|
||||
/// Also returns if the EXA_COLORS variable should reset the existing file
|
||||
/// type mappings or not. The `reset` code needs to be the first one.
|
||||
fn parse_color_vars<V: Vars>(vars: &V, colours: &mut Colours) -> (ExtensionMappings, bool) {
|
||||
use options::vars;
|
||||
use style::LSColors;
|
||||
|
||||
let mut exts = ExtensionMappings::default();
|
||||
|
||||
if let Some(lsc) = vars.get(vars::LS_COLORS) {
|
||||
let lsc = lsc.to_string_lossy();
|
||||
LSColors(lsc.as_ref()).each_pair(|pair| {
|
||||
if !colours.set_ls(&pair) {
|
||||
match glob::Pattern::new(pair.key) {
|
||||
Ok(pat) => exts.add(pat, pair.to_style()),
|
||||
Err(e) => warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut use_default_filetypes = true;
|
||||
|
||||
if let Some(exa) = vars.get(vars::EXA_COLORS) {
|
||||
let exa = exa.to_string_lossy();
|
||||
|
||||
// Is this hacky? Yes.
|
||||
if exa == "reset" || exa.starts_with("reset:") {
|
||||
use_default_filetypes = false;
|
||||
}
|
||||
|
||||
LSColors(exa.as_ref()).each_pair(|pair| {
|
||||
if !colours.set_ls(&pair) && !colours.set_exa(&pair) {
|
||||
match glob::Pattern::new(pair.key) {
|
||||
Ok(pat) => exts.add(pat, pair.to_style()),
|
||||
Err(e) => warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e),
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
(exts, use_default_filetypes)
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Default)]
|
||||
struct ExtensionMappings {
|
||||
mappings: Vec<(glob::Pattern, Style)>
|
||||
}
|
||||
|
||||
// Loop through backwards so that colours specified later in the list override
|
||||
// colours specified earlier, like we do with options and strict mode
|
||||
|
||||
use output::file_name::FileColours;
|
||||
impl FileColours for ExtensionMappings {
|
||||
fn colour_file(&self, file: &File) -> Option<Style> {
|
||||
self.mappings
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|t| t.0.matches(&file.name))
|
||||
.map (|t| t.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionMappings {
|
||||
fn is_non_empty(&self) -> bool {
|
||||
!self.mappings.is_empty()
|
||||
}
|
||||
|
||||
fn add(&mut self, pattern: glob::Pattern, style: Style) {
|
||||
self.mappings.push((pattern, style))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Classify {
|
||||
fn deduce(matches: &MatchedFlags) -> Result<Classify, Misfire> {
|
||||
let flagged = matches.has(&flags::CLASSIFY)?;
|
||||
|
||||
Ok(if flagged { Classify::AddFileIndicators }
|
||||
else { Classify::JustFilenames })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod terminal_test {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
use options::flags;
|
||||
use options::parser::{Flag, Arg};
|
||||
|
||||
use options::test::parse_for_test;
|
||||
use options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR ];
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $inputs:expr; $stricts:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| TerminalColours::deduce(mf)) {
|
||||
assert_eq!(result, $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $inputs:expr; $stricts:expr => err $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| TerminalColours::deduce(mf)) {
|
||||
assert_eq!(result.unwrap_err(), $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Default
|
||||
test!(empty: []; Both => Ok(TerminalColours::default()));
|
||||
|
||||
// --colour
|
||||
test!(u_always: ["--colour=always"]; Both => Ok(TerminalColours::Always));
|
||||
test!(u_auto: ["--colour", "auto"]; Both => Ok(TerminalColours::Automatic));
|
||||
test!(u_never: ["--colour=never"]; Both => Ok(TerminalColours::Never));
|
||||
|
||||
// --color
|
||||
test!(no_u_always: ["--color", "always"]; Both => Ok(TerminalColours::Always));
|
||||
test!(no_u_auto: ["--color=auto"]; Both => Ok(TerminalColours::Automatic));
|
||||
test!(no_u_never: ["--color", "never"]; Both => Ok(TerminalColours::Never));
|
||||
|
||||
// Errors
|
||||
test!(no_u_error: ["--color=upstream"]; Both => err Misfire::BadArgument(&flags::COLOR, OsString::from("upstream"))); // the error is for --color
|
||||
test!(u_error: ["--colour=lovers"]; Both => err Misfire::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one!
|
||||
|
||||
// Overriding
|
||||
test!(overridden_1: ["--colour=auto", "--colour=never"]; Last => Ok(TerminalColours::Never));
|
||||
test!(overridden_2: ["--color=auto", "--colour=never"]; Last => Ok(TerminalColours::Never));
|
||||
test!(overridden_3: ["--colour=auto", "--color=never"]; Last => Ok(TerminalColours::Never));
|
||||
test!(overridden_4: ["--color=auto", "--color=never"]; Last => Ok(TerminalColours::Never));
|
||||
|
||||
test!(overridden_5: ["--colour=auto", "--colour=never"]; Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("colour")));
|
||||
test!(overridden_6: ["--color=auto", "--colour=never"]; Complain => err Misfire::Duplicate(Flag::Long("color"), Flag::Long("colour")));
|
||||
test!(overridden_7: ["--colour=auto", "--color=never"]; Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("color")));
|
||||
test!(overridden_8: ["--color=auto", "--color=never"]; Complain => err Misfire::Duplicate(Flag::Long("color"), Flag::Long("color")));
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod colour_test {
|
||||
use super::*;
|
||||
use options::flags;
|
||||
use options::parser::{Flag, Arg};
|
||||
|
||||
use options::test::parse_for_test;
|
||||
use options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR,
|
||||
&flags::COLOR_SCALE, &flags::COLOUR_SCALE ];
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $inputs:expr, $widther:expr; $stricts:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
|
||||
assert_eq!(result, $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $inputs:expr, $widther:expr; $stricts:expr => err $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
|
||||
assert_eq!(result.unwrap_err(), $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $inputs:expr, $widther:expr; $stricts:expr => like $pat:pat) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
|
||||
println!("Testing {:?}", result);
|
||||
match result {
|
||||
$pat => assert!(true),
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test!(width_1: ["--colour", "always"], || Some(80); Both => Ok(Colours::colourful(false)));
|
||||
test!(width_2: ["--colour", "always"], || None; Both => Ok(Colours::colourful(false)));
|
||||
test!(width_3: ["--colour", "never"], || Some(80); Both => Ok(Colours::plain()));
|
||||
test!(width_4: ["--colour", "never"], || None; Both => Ok(Colours::plain()));
|
||||
test!(width_5: ["--colour", "automatic"], || Some(80); Both => Ok(Colours::colourful(false)));
|
||||
test!(width_6: ["--colour", "automatic"], || None; Both => Ok(Colours::plain()));
|
||||
test!(width_7: [], || Some(80); Both => Ok(Colours::colourful(false)));
|
||||
test!(width_8: [], || None; Both => Ok(Colours::plain()));
|
||||
|
||||
test!(scale_1: ["--color=always", "--color-scale", "--colour-scale"], || None; Last => like Ok(Colours { scale: true, .. }));
|
||||
test!(scale_2: ["--color=always", "--color-scale", ], || None; Last => like Ok(Colours { scale: true, .. }));
|
||||
test!(scale_3: ["--color=always", "--colour-scale"], || None; Last => like Ok(Colours { scale: true, .. }));
|
||||
test!(scale_4: ["--color=always", ], || None; Last => like Ok(Colours { scale: false, .. }));
|
||||
|
||||
test!(scale_5: ["--color=always", "--color-scale", "--colour-scale"], || None; Complain => err Misfire::Duplicate(Flag::Long("color-scale"), Flag::Long("colour-scale")));
|
||||
test!(scale_6: ["--color=always", "--color-scale", ], || None; Complain => like Ok(Colours { scale: true, .. }));
|
||||
test!(scale_7: ["--color=always", "--colour-scale"], || None; Complain => like Ok(Colours { scale: true, .. }));
|
||||
test!(scale_8: ["--color=always", ], || None; Complain => like Ok(Colours { scale: false, .. }));
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod customs_test {
|
||||
use std::ffi::OsString;
|
||||
|
||||
use super::*;
|
||||
use options::Vars;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: ls $ls:expr, exa $exa:expr => colours $expected:ident -> $process_expected:expr) => {
|
||||
#[test]
|
||||
#[allow(unused_mut)]
|
||||
fn $name() {
|
||||
let mut $expected = Colours::colourful(false);
|
||||
$process_expected();
|
||||
|
||||
let vars = MockVars { ls: $ls, exa: $exa };
|
||||
|
||||
let mut result = Colours::colourful(false);
|
||||
let (_exts, _reset) = parse_color_vars(&vars, &mut result);
|
||||
assert_eq!($expected, result);
|
||||
}
|
||||
};
|
||||
($name:ident: ls $ls:expr, exa $exa:expr => exts $mappings:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mappings: Vec<(glob::Pattern, Style)>
|
||||
= $mappings.into_iter()
|
||||
.map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
|
||||
.collect();
|
||||
|
||||
let vars = MockVars { ls: $ls, exa: $exa };
|
||||
|
||||
let mut meh = Colours::colourful(false);
|
||||
let (result, _reset) = parse_color_vars(&vars, &mut meh);
|
||||
assert_eq!(ExtensionMappings { mappings }, result);
|
||||
}
|
||||
};
|
||||
($name:ident: ls $ls:expr, exa $exa:expr => colours $expected:ident -> $process_expected:expr, exts $mappings:expr) => {
|
||||
#[test]
|
||||
#[allow(unused_mut)]
|
||||
fn $name() {
|
||||
let mut $expected = Colours::colourful(false);
|
||||
$process_expected();
|
||||
|
||||
let mappings: Vec<(glob::Pattern, Style)>
|
||||
= $mappings.into_iter()
|
||||
.map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
|
||||
.collect();
|
||||
|
||||
let vars = MockVars { ls: $ls, exa: $exa };
|
||||
|
||||
let mut meh = Colours::colourful(false);
|
||||
let (result, _reset) = parse_color_vars(&vars, &mut meh);
|
||||
assert_eq!(ExtensionMappings { mappings }, result);
|
||||
assert_eq!($expected, meh);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct MockVars {
|
||||
ls: &'static str,
|
||||
exa: &'static str,
|
||||
}
|
||||
|
||||
// Test impl that just returns the value it has.
|
||||
impl Vars for MockVars {
|
||||
fn get(&self, name: &'static str) -> Option<OsString> {
|
||||
use options::vars;
|
||||
|
||||
if name == vars::LS_COLORS && !self.ls.is_empty() {
|
||||
OsString::from(self.ls.clone()).into()
|
||||
}
|
||||
else if name == vars::EXA_COLORS && !self.exa.is_empty() {
|
||||
OsString::from(self.exa.clone()).into()
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LS_COLORS can affect all of these colours:
|
||||
test!(ls_di: ls "di=31", exa "" => colours c -> { c.filekinds.directory = Red.normal(); });
|
||||
test!(ls_ex: ls "ex=32", exa "" => colours c -> { c.filekinds.executable = Green.normal(); });
|
||||
test!(ls_fi: ls "fi=33", exa "" => colours c -> { c.filekinds.normal = Yellow.normal(); });
|
||||
test!(ls_pi: ls "pi=34", exa "" => colours c -> { c.filekinds.pipe = Blue.normal(); });
|
||||
test!(ls_so: ls "so=35", exa "" => colours c -> { c.filekinds.socket = Purple.normal(); });
|
||||
test!(ls_bd: ls "bd=36", exa "" => colours c -> { c.filekinds.block_device = Cyan.normal(); });
|
||||
test!(ls_cd: ls "cd=35", exa "" => colours c -> { c.filekinds.char_device = Purple.normal(); });
|
||||
test!(ls_ln: ls "ln=34", exa "" => colours c -> { c.filekinds.symlink = Blue.normal(); });
|
||||
test!(ls_or: ls "or=33", exa "" => colours c -> { c.broken_symlink = Yellow.normal(); });
|
||||
|
||||
// EXA_COLORS can affect all those colours too:
|
||||
test!(exa_di: ls "", exa "di=32" => colours c -> { c.filekinds.directory = Green.normal(); });
|
||||
test!(exa_ex: ls "", exa "ex=33" => colours c -> { c.filekinds.executable = Yellow.normal(); });
|
||||
test!(exa_fi: ls "", exa "fi=34" => colours c -> { c.filekinds.normal = Blue.normal(); });
|
||||
test!(exa_pi: ls "", exa "pi=35" => colours c -> { c.filekinds.pipe = Purple.normal(); });
|
||||
test!(exa_so: ls "", exa "so=36" => colours c -> { c.filekinds.socket = Cyan.normal(); });
|
||||
test!(exa_bd: ls "", exa "bd=35" => colours c -> { c.filekinds.block_device = Purple.normal(); });
|
||||
test!(exa_cd: ls "", exa "cd=34" => colours c -> { c.filekinds.char_device = Blue.normal(); });
|
||||
test!(exa_ln: ls "", exa "ln=33" => colours c -> { c.filekinds.symlink = Yellow.normal(); });
|
||||
test!(exa_or: ls "", exa "or=32" => colours c -> { c.broken_symlink = Green.normal(); });
|
||||
|
||||
// EXA_COLORS will even override options from LS_COLORS:
|
||||
test!(ls_exa_di: ls "di=31", exa "di=32" => colours c -> { c.filekinds.directory = Green.normal(); });
|
||||
test!(ls_exa_ex: ls "ex=32", exa "ex=33" => colours c -> { c.filekinds.executable = Yellow.normal(); });
|
||||
test!(ls_exa_fi: ls "fi=33", exa "fi=34" => colours c -> { c.filekinds.normal = Blue.normal(); });
|
||||
|
||||
// But more importantly, EXA_COLORS has its own, special list of colours:
|
||||
test!(exa_ur: ls "", exa "ur=38;5;100" => colours c -> { c.perms.user_read = Fixed(100).normal(); });
|
||||
test!(exa_uw: ls "", exa "uw=38;5;101" => colours c -> { c.perms.user_write = Fixed(101).normal(); });
|
||||
test!(exa_ux: ls "", exa "ux=38;5;102" => colours c -> { c.perms.user_execute_file = Fixed(102).normal(); });
|
||||
test!(exa_ue: ls "", exa "ue=38;5;103" => colours c -> { c.perms.user_execute_other = Fixed(103).normal(); });
|
||||
test!(exa_gr: ls "", exa "gr=38;5;104" => colours c -> { c.perms.group_read = Fixed(104).normal(); });
|
||||
test!(exa_gw: ls "", exa "gw=38;5;105" => colours c -> { c.perms.group_write = Fixed(105).normal(); });
|
||||
test!(exa_gx: ls "", exa "gx=38;5;106" => colours c -> { c.perms.group_execute = Fixed(106).normal(); });
|
||||
test!(exa_tr: ls "", exa "tr=38;5;107" => colours c -> { c.perms.other_read = Fixed(107).normal(); });
|
||||
test!(exa_tw: ls "", exa "tw=38;5;108" => colours c -> { c.perms.other_write = Fixed(108).normal(); });
|
||||
test!(exa_tx: ls "", exa "tx=38;5;109" => colours c -> { c.perms.other_execute = Fixed(109).normal(); });
|
||||
test!(exa_su: ls "", exa "su=38;5;110" => colours c -> { c.perms.special_user_file = Fixed(110).normal(); });
|
||||
test!(exa_sf: ls "", exa "sf=38;5;111" => colours c -> { c.perms.special_other = Fixed(111).normal(); });
|
||||
test!(exa_xa: ls "", exa "xa=38;5;112" => colours c -> { c.perms.attribute = Fixed(112).normal(); });
|
||||
|
||||
test!(exa_sn: ls "", exa "sn=38;5;113" => colours c -> { c.size.numbers = Fixed(113).normal(); });
|
||||
test!(exa_sb: ls "", exa "sb=38;5;114" => colours c -> { c.size.unit = Fixed(114).normal(); });
|
||||
test!(exa_df: ls "", exa "df=38;5;115" => colours c -> { c.size.major = Fixed(115).normal(); });
|
||||
test!(exa_ds: ls "", exa "ds=38;5;116" => colours c -> { c.size.minor = Fixed(116).normal(); });
|
||||
|
||||
test!(exa_uu: ls "", exa "uu=38;5;117" => colours c -> { c.users.user_you = Fixed(117).normal(); });
|
||||
test!(exa_un: ls "", exa "un=38;5;118" => colours c -> { c.users.user_someone_else = Fixed(118).normal(); });
|
||||
test!(exa_gu: ls "", exa "gu=38;5;119" => colours c -> { c.users.group_yours = Fixed(119).normal(); });
|
||||
test!(exa_gn: ls "", exa "gn=38;5;120" => colours c -> { c.users.group_not_yours = Fixed(120).normal(); });
|
||||
|
||||
test!(exa_lc: ls "", exa "lc=38;5;121" => colours c -> { c.links.normal = Fixed(121).normal(); });
|
||||
test!(exa_lm: ls "", exa "lm=38;5;122" => colours c -> { c.links.multi_link_file = Fixed(122).normal(); });
|
||||
|
||||
test!(exa_ga: ls "", exa "ga=38;5;123" => colours c -> { c.git.new = Fixed(123).normal(); });
|
||||
test!(exa_gm: ls "", exa "gm=38;5;124" => colours c -> { c.git.modified = Fixed(124).normal(); });
|
||||
test!(exa_gd: ls "", exa "gd=38;5;125" => colours c -> { c.git.deleted = Fixed(125).normal(); });
|
||||
test!(exa_gv: ls "", exa "gv=38;5;126" => colours c -> { c.git.renamed = Fixed(126).normal(); });
|
||||
test!(exa_gt: ls "", exa "gt=38;5;127" => colours c -> { c.git.typechange = Fixed(127).normal(); });
|
||||
|
||||
test!(exa_xx: ls "", exa "xx=38;5;128" => colours c -> { c.punctuation = Fixed(128).normal(); });
|
||||
test!(exa_da: ls "", exa "da=38;5;129" => colours c -> { c.date = Fixed(129).normal(); });
|
||||
test!(exa_in: ls "", exa "in=38;5;130" => colours c -> { c.inode = Fixed(130).normal(); });
|
||||
test!(exa_bl: ls "", exa "bl=38;5;131" => colours c -> { c.blocks = Fixed(131).normal(); });
|
||||
test!(exa_hd: ls "", exa "hd=38;5;132" => colours c -> { c.header = Fixed(132).normal(); });
|
||||
test!(exa_lp: ls "", exa "lp=38;5;133" => colours c -> { c.symlink_path = Fixed(133).normal(); });
|
||||
test!(exa_cc: ls "", exa "cc=38;5;134" => colours c -> { c.control_char = Fixed(134).normal(); });
|
||||
test!(exa_bo: ls "", exa "bO=4" => colours c -> { c.broken_path_overlay = Style::default().underline(); });
|
||||
|
||||
// All the while, LS_COLORS treats them as filenames:
|
||||
test!(ls_uu: ls "uu=38;5;117", exa "" => exts [ ("uu", Fixed(117).normal()) ]);
|
||||
test!(ls_un: ls "un=38;5;118", exa "" => exts [ ("un", Fixed(118).normal()) ]);
|
||||
test!(ls_gu: ls "gu=38;5;119", exa "" => exts [ ("gu", Fixed(119).normal()) ]);
|
||||
test!(ls_gn: ls "gn=38;5;120", exa "" => exts [ ("gn", Fixed(120).normal()) ]);
|
||||
|
||||
// Just like all other keys:
|
||||
test!(ls_txt: ls "*.txt=31", exa "" => exts [ ("*.txt", Red.normal()) ]);
|
||||
test!(ls_mp3: ls "*.mp3=38;5;135", exa "" => exts [ ("*.mp3", Fixed(135).normal()) ]);
|
||||
test!(ls_mak: ls "Makefile=1;32;4", exa "" => exts [ ("Makefile", Green.bold().underline()) ]);
|
||||
test!(exa_txt: ls "", exa "*.zip=31" => exts [ ("*.zip", Red.normal()) ]);
|
||||
test!(exa_mp3: ls "", exa "lev.*=38;5;153" => exts [ ("lev.*", Fixed(153).normal()) ]);
|
||||
test!(exa_mak: ls "", exa "Cargo.toml=4;32;1" => exts [ ("Cargo.toml", Green.bold().underline()) ]);
|
||||
|
||||
// Testing whether a glob from EXA_COLORS overrides a glob from LS_COLORS
|
||||
// can’t be tested here, because they’ll both be added to the same vec
|
||||
|
||||
// Values get separated by colons:
|
||||
test!(ls_multi: ls "*.txt=31:*.rtf=32", exa "" => exts [ ("*.txt", Red.normal()), ("*.rtf", Green.normal()) ]);
|
||||
test!(exa_multi: ls "", exa "*.tmp=37:*.log=37" => exts [ ("*.tmp", White.normal()), ("*.log", White.normal()) ]);
|
||||
|
||||
test!(ls_five: ls "1*1=31:2*2=32:3*3=1;33:4*4=34;1:5*5=35;4", exa "" => exts [
|
||||
("1*1", Red.normal()), ("2*2", Green.normal()), ("3*3", Yellow.bold()), ("4*4", Blue.bold()), ("5*5", Purple.underline())
|
||||
]);
|
||||
|
||||
// Finally, colours get applied right-to-left:
|
||||
test!(ls_overwrite: ls "pi=31:pi=32:pi=33", exa "" => colours c -> { c.filekinds.pipe = Yellow.normal(); });
|
||||
test!(exa_overwrite: ls "", exa "da=36:da=35:da=34" => colours c -> { c.date = Blue.normal(); });
|
||||
}
|
55
src/options/vars.rs
Normal file
55
src/options/vars.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use std::ffi::OsString;
|
||||
|
||||
|
||||
// General variables
|
||||
|
||||
/// Environment variable used to colour files, both by their filesystem type
|
||||
/// (symlink, socket, directory) and their file name or extension (image,
|
||||
/// video, archive);
|
||||
pub static LS_COLORS: &str = "LS_COLORS";
|
||||
|
||||
/// Environment variable used to override the width of the terminal, in
|
||||
/// characters.
|
||||
pub static COLUMNS: &str = "COLUMNS";
|
||||
|
||||
/// Environment variable used to datetime format.
|
||||
pub static TIME_STYLE: &str = "TIME_STYLE";
|
||||
|
||||
// exa-specific variables
|
||||
|
||||
/// Environment variable used to colour exa’s interface when colours are
|
||||
/// enabled. This includes all the colours that LS_COLORS would recognise,
|
||||
/// overriding them if necessary. It can also contain exa-specific codes.
|
||||
pub static EXA_COLORS: &str = "EXA_COLORS";
|
||||
|
||||
/// Environment variable used to switch on strict argument checking, such as
|
||||
/// complaining if an argument was specified twice, or if two conflict.
|
||||
/// This is meant to be so you don’t accidentally introduce the wrong
|
||||
/// behaviour in a script, rather than for general command-line use.
|
||||
/// Any non-empty value will turn strict mode on.
|
||||
pub static EXA_STRICT: &str = "EXA_STRICT";
|
||||
|
||||
/// Environment variable used to make exa print out debugging information as
|
||||
/// it runs. Any non-empty value will turn debug mode on.
|
||||
pub static EXA_DEBUG: &str = "EXA_DEBUG";
|
||||
|
||||
/// Environment variable used to limit the grid-details view
|
||||
/// (`--grid --long`) so it’s only activated if there’s at least the given
|
||||
/// number of rows of output.
|
||||
pub static EXA_GRID_ROWS: &str = "EXA_GRID_ROWS";
|
||||
|
||||
|
||||
|
||||
/// Mockable wrapper for `std::env::var_os`.
|
||||
pub trait Vars {
|
||||
fn get(&self, name: &'static str) -> Option<OsString>;
|
||||
}
|
||||
|
||||
|
||||
// Test impl that just returns the value it has.
|
||||
#[cfg(test)]
|
||||
impl Vars for Option<OsString> {
|
||||
fn get(&self, _name: &'static str) -> Option<OsString> {
|
||||
self.clone()
|
||||
}
|
||||
}
|
56
src/options/version.rs
Normal file
56
src/options/version.rs
Normal file
@ -0,0 +1,56 @@
|
||||
//! Printing the version string.
|
||||
//!
|
||||
//! The code that works out which string to print is done in `build.rs`.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use options::flags;
|
||||
use options::parser::MatchedFlags;
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct VersionString;
|
||||
// There were options here once, but there aren’t anymore!
|
||||
|
||||
impl VersionString {
|
||||
|
||||
/// Determines how to show the version, if at all, based on the user’s
|
||||
/// command-line arguments. This one works backwards from the other
|
||||
/// ‘deduce’ functions, returning Err if help needs to be shown.
|
||||
///
|
||||
/// Like --help, this doesn’t bother checking for errors.
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<(), VersionString> {
|
||||
if matches.count(&flags::VERSION) > 0 {
|
||||
Err(VersionString)
|
||||
}
|
||||
else {
|
||||
Ok(()) // no version needs to be shown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionString {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
write!(f, "{}", include!(concat!(env!("OUT_DIR"), "/version_string.txt")))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use options::Options;
|
||||
use std::ffi::OsString;
|
||||
|
||||
fn os(input: &'static str) -> OsString {
|
||||
let mut os = OsString::new();
|
||||
os.push(input);
|
||||
os
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
let args = [ os("--version") ];
|
||||
let opts = Options::parse(&args, &None);
|
||||
assert!(opts.is_err())
|
||||
}
|
||||
}
|
@ -1,151 +1,138 @@
|
||||
use std::env::var_os;
|
||||
|
||||
use getopts;
|
||||
|
||||
use info::filetype::FileExtensions;
|
||||
use output::Colours;
|
||||
use output::{grid, details};
|
||||
use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions};
|
||||
use output::file_name::{Classify, FileStyle};
|
||||
use output::{View, Mode, grid, details, lines};
|
||||
use output::grid_details::{self, RowThreshold};
|
||||
use output::table::{TimeTypes, Environment, SizeFormat, Columns, Options as TableOptions};
|
||||
use output::time::TimeFormat;
|
||||
use options::Misfire;
|
||||
|
||||
use options::{flags, Misfire, Vars};
|
||||
use options::parser::MatchedFlags;
|
||||
|
||||
use fs::PlatformMetadata;
|
||||
use fs::feature::xattr;
|
||||
|
||||
|
||||
/// The **view** contains all information about how to format output.
|
||||
#[derive(Debug)]
|
||||
pub struct View {
|
||||
pub mode: Mode,
|
||||
pub colours: Colours,
|
||||
pub style: FileStyle,
|
||||
}
|
||||
|
||||
impl View {
|
||||
|
||||
/// Determine which view to use and all of that view’s arguments.
|
||||
pub fn deduce(matches: &getopts::Matches) -> Result<View, Misfire> {
|
||||
let mode = Mode::deduce(matches)?;
|
||||
let colours = Colours::deduce(matches)?;
|
||||
let style = FileStyle::deduce(matches);
|
||||
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<View, Misfire> {
|
||||
use options::style::Styles;
|
||||
|
||||
let mode = Mode::deduce(matches, vars)?;
|
||||
let Styles { colours, style } = Styles::deduce(matches, vars, || *TERM_WIDTH)?;
|
||||
Ok(View { mode, colours, style })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The **mode** is the “type” of output.
|
||||
#[derive(Debug)]
|
||||
pub enum Mode {
|
||||
Grid(grid::Options),
|
||||
Details(details::Options),
|
||||
GridDetails(grid::Options, details::Options),
|
||||
Lines,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
|
||||
/// Determine the mode from the command-line arguments.
|
||||
pub fn deduce(matches: &getopts::Matches) -> Result<Mode, Misfire> {
|
||||
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Mode, Misfire> {
|
||||
use options::misfire::Misfire::*;
|
||||
|
||||
let long = || {
|
||||
if matches.opt_present("across") && !matches.opt_present("grid") {
|
||||
Err(Useless("across", true, "long"))
|
||||
if matches.has(&flags::ACROSS)? && !matches.has(&flags::GRID)? {
|
||||
Err(Useless(&flags::ACROSS, true, &flags::LONG))
|
||||
}
|
||||
else if matches.opt_present("oneline") {
|
||||
Err(Useless("oneline", true, "long"))
|
||||
else if matches.has(&flags::ONE_LINE)? {
|
||||
Err(Useless(&flags::ONE_LINE, true, &flags::LONG))
|
||||
}
|
||||
else {
|
||||
Ok(details::Options {
|
||||
table: Some(TableOptions::deduce(matches)?),
|
||||
header: matches.opt_present("header"),
|
||||
xattr: xattr::ENABLED && matches.opt_present("extended"),
|
||||
table: Some(TableOptions::deduce(matches, vars)?),
|
||||
header: matches.has(&flags::HEADER)?,
|
||||
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
|
||||
icons: matches.has(&flags::ICONS)?,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let long_options_scan = || {
|
||||
for option in &[ "binary", "bytes", "inode", "links", "header", "blocks", "time", "group" ] {
|
||||
if matches.opt_present(option) {
|
||||
return Err(Useless(option, false, "long"));
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(feature="git") && matches.opt_present("git") {
|
||||
Err(Useless("git", false, "long"))
|
||||
}
|
||||
else if matches.opt_present("level") && !matches.opt_present("recurse") && !matches.opt_present("tree") {
|
||||
Err(Useless2("level", "recurse", "tree"))
|
||||
}
|
||||
else if xattr::ENABLED && matches.opt_present("extended") {
|
||||
Err(Useless("extended", false, "long"))
|
||||
}
|
||||
else {
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
let other_options_scan = || {
|
||||
if let Some(width) = TerminalWidth::deduce()?.width() {
|
||||
if matches.opt_present("oneline") {
|
||||
if matches.opt_present("across") {
|
||||
Err(Useless("across", true, "oneline"))
|
||||
if let Some(width) = TerminalWidth::deduce(vars)?.width() {
|
||||
if matches.has(&flags::ONE_LINE)? {
|
||||
if matches.has(&flags::ACROSS)? {
|
||||
Err(Useless(&flags::ACROSS, true, &flags::ONE_LINE))
|
||||
}
|
||||
else {
|
||||
Ok(Mode::Lines)
|
||||
let lines = lines::Options { icons: matches.has(&flags::ICONS)? };
|
||||
Ok(Mode::Lines(lines))
|
||||
}
|
||||
}
|
||||
else if matches.opt_present("tree") {
|
||||
else if matches.has(&flags::TREE)? {
|
||||
let details = details::Options {
|
||||
table: None,
|
||||
header: false,
|
||||
xattr: false,
|
||||
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
|
||||
icons: matches.has(&flags::ICONS)?,
|
||||
};
|
||||
|
||||
Ok(Mode::Details(details))
|
||||
}
|
||||
else {
|
||||
let grid = grid::Options {
|
||||
across: matches.opt_present("across"),
|
||||
across: matches.has(&flags::ACROSS)?,
|
||||
console_width: width,
|
||||
icons: matches.has(&flags::ICONS)?,
|
||||
};
|
||||
|
||||
Ok(Mode::Grid(grid))
|
||||
}
|
||||
}
|
||||
|
||||
// If the terminal width couldn’t be matched for some reason, such
|
||||
// as the program’s stdout being connected to a file, then
|
||||
// fallback to the lines view.
|
||||
else if matches.has(&flags::TREE)? {
|
||||
let details = details::Options {
|
||||
table: None,
|
||||
header: false,
|
||||
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
|
||||
icons: matches.has(&flags::ICONS)?,
|
||||
};
|
||||
|
||||
Ok(Mode::Details(details))
|
||||
}
|
||||
else {
|
||||
// If the terminal width couldn’t be matched for some reason, such
|
||||
// as the program’s stdout being connected to a file, then
|
||||
// fallback to the lines view.
|
||||
|
||||
if matches.opt_present("tree") {
|
||||
let details = details::Options {
|
||||
table: None,
|
||||
header: false,
|
||||
xattr: false,
|
||||
};
|
||||
|
||||
Ok(Mode::Details(details))
|
||||
}
|
||||
else {
|
||||
Ok(Mode::Lines)
|
||||
}
|
||||
let lines = lines::Options { icons: matches.has(&flags::ICONS)?, };
|
||||
Ok(Mode::Lines(lines))
|
||||
}
|
||||
};
|
||||
|
||||
if matches.opt_present("long") {
|
||||
if matches.has(&flags::LONG)? {
|
||||
let details = long()?;
|
||||
if matches.opt_present("grid") {
|
||||
match other_options_scan()? {
|
||||
Mode::Grid(grid) => return Ok(Mode::GridDetails(grid, details)),
|
||||
others => return Ok(others),
|
||||
};
|
||||
if matches.has(&flags::GRID)? {
|
||||
let other_options_mode = other_options_scan()?;
|
||||
if let Mode::Grid(grid) = other_options_mode {
|
||||
let row_threshold = RowThreshold::deduce(vars)?;
|
||||
return Ok(Mode::GridDetails(grid_details::Options { grid, details, row_threshold }));
|
||||
}
|
||||
else {
|
||||
return Ok(other_options_mode);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return Ok(Mode::Details(details));
|
||||
}
|
||||
}
|
||||
|
||||
long_options_scan()?;
|
||||
// If --long hasn’t been passed, then check if we need to warn the
|
||||
// user about flags that won’t have any effect.
|
||||
if matches.is_strict() {
|
||||
for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS,
|
||||
&flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] {
|
||||
if matches.has(option)? {
|
||||
return Err(Useless(*option, false, &flags::LONG));
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(feature="git") && matches.has(&flags::GIT)? {
|
||||
return Err(Useless(&flags::GIT, false, &flags::LONG));
|
||||
}
|
||||
else if matches.has(&flags::LEVEL)? && !matches.has(&flags::RECURSE)? && !matches.has(&flags::TREE)? {
|
||||
// TODO: I'm not sure if the code even gets this far.
|
||||
// There is an identical check in dir_action
|
||||
return Err(Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
|
||||
}
|
||||
}
|
||||
|
||||
other_options_scan()
|
||||
}
|
||||
@ -171,8 +158,10 @@ impl TerminalWidth {
|
||||
/// Determine a requested terminal width from the command-line arguments.
|
||||
///
|
||||
/// Returns an error if a requested width doesn’t parse to an integer.
|
||||
fn deduce() -> Result<TerminalWidth, Misfire> {
|
||||
if let Some(columns) = var_os("COLUMNS").and_then(|s| s.into_string().ok()) {
|
||||
fn deduce<V: Vars>(vars: &V) -> Result<TerminalWidth, Misfire> {
|
||||
use options::vars;
|
||||
|
||||
if let Some(columns) = vars.get(vars::COLUMNS).and_then(|s| s.into_string().ok()) {
|
||||
match columns.parse() {
|
||||
Ok(width) => Ok(TerminalWidth::Set(width)),
|
||||
Err(e) => Err(Misfire::FailedParse(e)),
|
||||
@ -196,19 +185,48 @@ impl TerminalWidth {
|
||||
}
|
||||
|
||||
|
||||
impl RowThreshold {
|
||||
|
||||
/// Determine whether to use a row threshold based on the given
|
||||
/// environment variables.
|
||||
fn deduce<V: Vars>(vars: &V) -> Result<RowThreshold, Misfire> {
|
||||
use options::vars;
|
||||
|
||||
if let Some(columns) = vars.get(vars::EXA_GRID_ROWS).and_then(|s| s.into_string().ok()) {
|
||||
match columns.parse() {
|
||||
Ok(rows) => Ok(RowThreshold::MinimumRows(rows)),
|
||||
Err(e) => Err(Misfire::FailedParse(e)),
|
||||
}
|
||||
}
|
||||
else {
|
||||
Ok(RowThreshold::AlwaysGrid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TableOptions {
|
||||
fn deduce(matches: &getopts::Matches) -> Result<Self, Misfire> {
|
||||
Ok(TableOptions {
|
||||
env: Environment::load_all(),
|
||||
time_format: TimeFormat::deduce(matches)?,
|
||||
size_format: SizeFormat::deduce(matches)?,
|
||||
time_types: TimeTypes::deduce(matches)?,
|
||||
inode: matches.opt_present("inode"),
|
||||
links: matches.opt_present("links"),
|
||||
blocks: matches.opt_present("blocks"),
|
||||
group: matches.opt_present("group"),
|
||||
git: cfg!(feature="git") && matches.opt_present("git"),
|
||||
})
|
||||
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, Misfire> {
|
||||
let env = Environment::load_all();
|
||||
let time_format = TimeFormat::deduce(matches, vars)?;
|
||||
let size_format = SizeFormat::deduce(matches)?;
|
||||
let extra_columns = Columns::deduce(matches)?;
|
||||
Ok(TableOptions { env, time_format, size_format, extra_columns })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Columns {
|
||||
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
|
||||
let time_types = TimeTypes::deduce(matches)?;
|
||||
let git = cfg!(feature="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)?;
|
||||
|
||||
Ok(Columns { time_types, git, blocks, group, inode, links })
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,16 +241,14 @@ impl SizeFormat {
|
||||
/// strings of digits in your head. Changing the format to anything else
|
||||
/// involves the `--binary` or `--bytes` flags, and these conflict with
|
||||
/// each other.
|
||||
fn deduce(matches: &getopts::Matches) -> Result<SizeFormat, Misfire> {
|
||||
let binary = matches.opt_present("binary");
|
||||
let bytes = matches.opt_present("bytes");
|
||||
fn deduce(matches: &MatchedFlags) -> Result<SizeFormat, Misfire> {
|
||||
let flag = matches.has_where(|f| f.matches(&flags::BINARY) || f.matches(&flags::BYTES))?;
|
||||
|
||||
match (binary, bytes) {
|
||||
(true, true ) => Err(Misfire::Conflict("binary", "bytes")),
|
||||
(true, false) => Ok(SizeFormat::BinaryBytes),
|
||||
(false, true ) => Ok(SizeFormat::JustBytes),
|
||||
(false, false) => Ok(SizeFormat::DecimalBytes),
|
||||
}
|
||||
Ok(match flag {
|
||||
Some(f) if f.matches(&flags::BINARY) => SizeFormat::BinaryBytes,
|
||||
Some(f) if f.matches(&flags::BYTES) => SizeFormat::JustBytes,
|
||||
_ => SizeFormat::DecimalBytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,21 +256,34 @@ impl SizeFormat {
|
||||
impl TimeFormat {
|
||||
|
||||
/// Determine how time should be formatted in timestamp columns.
|
||||
fn deduce(matches: &getopts::Matches) -> Result<TimeFormat, Misfire> {
|
||||
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<TimeFormat, Misfire> {
|
||||
pub use output::time::{DefaultFormat, ISOFormat};
|
||||
const STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"];
|
||||
|
||||
if let Some(word) = matches.opt_str("time-style") {
|
||||
match &*word {
|
||||
"default" => Ok(TimeFormat::DefaultFormat(DefaultFormat::new())),
|
||||
"iso" => Ok(TimeFormat::ISOFormat(ISOFormat::new())),
|
||||
"long-iso" => Ok(TimeFormat::LongISO),
|
||||
"full-iso" => Ok(TimeFormat::FullISO),
|
||||
otherwise => Err(Misfire::bad_argument("time-style", otherwise, STYLES)),
|
||||
}
|
||||
let word = match matches.get(&flags::TIME_STYLE)? {
|
||||
Some(w) => w.to_os_string(),
|
||||
None => {
|
||||
use options::vars;
|
||||
match vars.get(vars::TIME_STYLE) {
|
||||
Some(ref t) if !t.is_empty() => t.clone(),
|
||||
_ => return Ok(TimeFormat::DefaultFormat(DefaultFormat::load()))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if &word == "default" {
|
||||
Ok(TimeFormat::DefaultFormat(DefaultFormat::load()))
|
||||
}
|
||||
else if &word == "iso" {
|
||||
Ok(TimeFormat::ISOFormat(ISOFormat::load()))
|
||||
}
|
||||
else if &word == "long-iso" {
|
||||
Ok(TimeFormat::LongISO)
|
||||
}
|
||||
else if &word == "full-iso" {
|
||||
Ok(TimeFormat::FullISO)
|
||||
}
|
||||
else {
|
||||
Ok(TimeFormat::DefaultFormat(DefaultFormat::new()))
|
||||
Err(Misfire::BadArgument(&flags::TIME_STYLE, word.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -272,118 +301,61 @@ impl TimeTypes {
|
||||
/// It’s valid to show more than one column by passing in more than one
|
||||
/// option, but passing *no* options means that the user just wants to
|
||||
/// see the default set.
|
||||
fn deduce(matches: &getopts::Matches) -> Result<TimeTypes, Misfire> {
|
||||
let possible_word = matches.opt_str("time");
|
||||
let modified = matches.opt_present("modified");
|
||||
let created = matches.opt_present("created");
|
||||
let accessed = matches.opt_present("accessed");
|
||||
fn deduce(matches: &MatchedFlags) -> Result<TimeTypes, Misfire> {
|
||||
let possible_word = matches.get(&flags::TIME)?;
|
||||
let modified = matches.has(&flags::MODIFIED)?;
|
||||
let changed = matches.has(&flags::CHANGED)?;
|
||||
let accessed = matches.has(&flags::ACCESSED)?;
|
||||
let created = matches.has(&flags::CREATED)?;
|
||||
|
||||
if let Some(word) = possible_word {
|
||||
let time_types = if let Some(word) = possible_word {
|
||||
if modified {
|
||||
return Err(Misfire::Useless("modified", true, "time"));
|
||||
return Err(Misfire::Useless(&flags::MODIFIED, true, &flags::TIME));
|
||||
}
|
||||
else if created {
|
||||
return Err(Misfire::Useless("created", true, "time"));
|
||||
else if changed {
|
||||
return Err(Misfire::Useless(&flags::CHANGED, true, &flags::TIME));
|
||||
}
|
||||
else if accessed {
|
||||
return Err(Misfire::Useless("accessed", true, "time"));
|
||||
return Err(Misfire::Useless(&flags::ACCESSED, true, &flags::TIME));
|
||||
}
|
||||
|
||||
static TIMES: &[& str] = &["modified", "accessed", "created"];
|
||||
match &*word {
|
||||
"mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }),
|
||||
"acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }),
|
||||
"cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }),
|
||||
otherwise => Err(Misfire::bad_argument("time", otherwise, TIMES))
|
||||
else if created {
|
||||
return Err(Misfire::Useless(&flags::CREATED, true, &flags::TIME));
|
||||
}
|
||||
else if word == "mod" || word == "modified" {
|
||||
TimeTypes { modified: true, changed: false, accessed: false, created: false }
|
||||
}
|
||||
else if word == "ch" || word == "changed" {
|
||||
TimeTypes { modified: false, changed: true, accessed: false, created: false }
|
||||
}
|
||||
else if word == "acc" || word == "accessed" {
|
||||
TimeTypes { modified: false, changed: false, accessed: true, created: false }
|
||||
}
|
||||
else if word == "cr" || word == "created" {
|
||||
TimeTypes { modified: false, changed: false, accessed: false, created: true }
|
||||
}
|
||||
else {
|
||||
return Err(Misfire::BadArgument(&flags::TIME, word.into()));
|
||||
}
|
||||
}
|
||||
else if modified || created || accessed {
|
||||
Ok(TimeTypes { accessed, modified, created })
|
||||
else if modified || changed || accessed || created {
|
||||
TimeTypes { modified, changed, accessed, created }
|
||||
}
|
||||
else {
|
||||
Ok(TimeTypes::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
TimeTypes::default()
|
||||
};
|
||||
|
||||
let mut fields = vec![];
|
||||
if time_types.modified { fields.push(PlatformMetadata::ModifiedTime); }
|
||||
if time_types.changed { fields.push(PlatformMetadata::ChangedTime); }
|
||||
if time_types.accessed { fields.push(PlatformMetadata::AccessedTime); }
|
||||
if time_types.created { fields.push(PlatformMetadata::CreatedTime); }
|
||||
|
||||
/// Under what circumstances we should display coloured, rather than plain,
|
||||
/// output to the terminal.
|
||||
///
|
||||
/// By default, we want to display the colours when stdout can display them.
|
||||
/// 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)]
|
||||
enum TerminalColours {
|
||||
|
||||
/// Display them even when output isn’t going to a terminal.
|
||||
Always,
|
||||
|
||||
/// Display them when output is going to a terminal, but not otherwise.
|
||||
Automatic,
|
||||
|
||||
/// Never display them, even when output is going to a terminal.
|
||||
Never,
|
||||
}
|
||||
|
||||
impl Default for TerminalColours {
|
||||
fn default() -> TerminalColours {
|
||||
TerminalColours::Automatic
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalColours {
|
||||
|
||||
/// Determine which terminal colour conditions to use.
|
||||
fn deduce(matches: &getopts::Matches) -> Result<TerminalColours, Misfire> {
|
||||
const COLOURS: &[&str] = &["always", "auto", "never"];
|
||||
|
||||
if let Some(word) = matches.opt_str("color").or_else(|| matches.opt_str("colour")) {
|
||||
match &*word {
|
||||
"always" => Ok(TerminalColours::Always),
|
||||
"auto" | "automatic" => Ok(TerminalColours::Automatic),
|
||||
"never" => Ok(TerminalColours::Never),
|
||||
otherwise => Err(Misfire::bad_argument("color", otherwise, COLOURS))
|
||||
for field in fields {
|
||||
if let Err(misfire) = field.check_supported() {
|
||||
return Err(misfire);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Ok(TerminalColours::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Colours {
|
||||
fn deduce(matches: &getopts::Matches) -> Result<Colours, Misfire> {
|
||||
use self::TerminalColours::*;
|
||||
|
||||
let tc = TerminalColours::deduce(matches)?;
|
||||
if tc == Always || (tc == Automatic && TERM_WIDTH.is_some()) {
|
||||
let scale = matches.opt_present("color-scale") || matches.opt_present("colour-scale");
|
||||
Ok(Colours::colourful(scale))
|
||||
}
|
||||
else {
|
||||
Ok(Colours::plain())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl FileStyle {
|
||||
fn deduce(matches: &getopts::Matches) -> FileStyle {
|
||||
let classify = Classify::deduce(matches);
|
||||
let exts = FileExtensions;
|
||||
FileStyle { classify, exts }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Classify {
|
||||
fn deduce(matches: &getopts::Matches) -> Classify {
|
||||
if matches.opt_present("classify") { Classify::AddFileIndicators }
|
||||
else { Classify::JustFilenames }
|
||||
Ok(time_types)
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,7 +365,258 @@ impl Classify {
|
||||
// so it’s easier to just cache it the first time it runs.
|
||||
lazy_static! {
|
||||
static ref TERM_WIDTH: Option<usize> = {
|
||||
use term::dimensions;
|
||||
dimensions().map(|t| t.0)
|
||||
// All of stdin, stdout, and stderr could not be connected to a
|
||||
// terminal, but we’re only interested in stdout because it’s
|
||||
// where the output goes.
|
||||
use term_size::dimensions_stdout;
|
||||
dimensions_stdout().map(|t| t.0)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
use options::flags;
|
||||
use options::parser::{Flag, Arg};
|
||||
|
||||
use options::test::parse_for_test;
|
||||
use options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES, &flags::TIME_STYLE,
|
||||
&flags::TIME, &flags::MODIFIED, &flags::CHANGED,
|
||||
&flags::CREATED, &flags::ACCESSED, &flags::ICONS,
|
||||
&flags::HEADER, &flags::GROUP, &flags::INODE, &flags::GIT,
|
||||
&flags::LINKS, &flags::BLOCKS, &flags::LONG, &flags::LEVEL,
|
||||
&flags::GRID, &flags::ACROSS, &flags::ONE_LINE ];
|
||||
|
||||
macro_rules! test {
|
||||
|
||||
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
|
||||
/// Macro that writes a test.
|
||||
/// If testing both strictnesses, they’ll both be done in the same function.
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
assert_eq!(result, $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($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.
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
assert_eq!(result.unwrap_err(), $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($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.
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
println!("Testing {:?}", result);
|
||||
match result {
|
||||
$pat => assert!(true),
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
($name:ident: $type:ident <- $inputs:expr, $vars:expr; $stricts:expr => err $result:expr) => {
|
||||
/// Like above, but with $vars.
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &$vars)) {
|
||||
assert_eq!(result.unwrap_err(), $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $type:ident <- $inputs:expr, $vars:expr; $stricts:expr => like $pat:pat) => {
|
||||
/// Like further above, but with $vars.
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &$vars)) {
|
||||
println!("Testing {:?}", result);
|
||||
match result {
|
||||
$pat => assert!(true),
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
mod size_formats {
|
||||
use super::*;
|
||||
|
||||
// Default behaviour
|
||||
test!(empty: SizeFormat <- []; Both => Ok(SizeFormat::DecimalBytes));
|
||||
|
||||
// Individual flags
|
||||
test!(binary: SizeFormat <- ["--binary"]; Both => Ok(SizeFormat::BinaryBytes));
|
||||
test!(bytes: SizeFormat <- ["--bytes"]; Both => Ok(SizeFormat::JustBytes));
|
||||
|
||||
// Overriding
|
||||
test!(both_1: SizeFormat <- ["--binary", "--binary"]; Last => Ok(SizeFormat::BinaryBytes));
|
||||
test!(both_2: SizeFormat <- ["--bytes", "--binary"]; Last => Ok(SizeFormat::BinaryBytes));
|
||||
test!(both_3: SizeFormat <- ["--binary", "--bytes"]; Last => Ok(SizeFormat::JustBytes));
|
||||
test!(both_4: SizeFormat <- ["--bytes", "--bytes"]; Last => Ok(SizeFormat::JustBytes));
|
||||
|
||||
test!(both_5: SizeFormat <- ["--binary", "--binary"]; Complain => err Misfire::Duplicate(Flag::Long("binary"), Flag::Long("binary")));
|
||||
test!(both_6: SizeFormat <- ["--bytes", "--binary"]; Complain => err Misfire::Duplicate(Flag::Long("bytes"), Flag::Long("binary")));
|
||||
test!(both_7: SizeFormat <- ["--binary", "--bytes"]; Complain => err Misfire::Duplicate(Flag::Long("binary"), Flag::Long("bytes")));
|
||||
test!(both_8: SizeFormat <- ["--bytes", "--bytes"]; Complain => err Misfire::Duplicate(Flag::Long("bytes"), Flag::Long("bytes")));
|
||||
}
|
||||
|
||||
|
||||
mod time_formats {
|
||||
use super::*;
|
||||
|
||||
// These tests use pattern matching because TimeFormat doesn’t
|
||||
// implement PartialEq.
|
||||
|
||||
// Default behaviour
|
||||
test!(empty: TimeFormat <- [], None; Both => like Ok(TimeFormat::DefaultFormat(_)));
|
||||
|
||||
// Individual settings
|
||||
test!(default: TimeFormat <- ["--time-style=default"], None; Both => like Ok(TimeFormat::DefaultFormat(_)));
|
||||
test!(iso: TimeFormat <- ["--time-style", "iso"], None; Both => like Ok(TimeFormat::ISOFormat(_)));
|
||||
test!(long_iso: TimeFormat <- ["--time-style=long-iso"], None; Both => like Ok(TimeFormat::LongISO));
|
||||
test!(full_iso: TimeFormat <- ["--time-style", "full-iso"], None; Both => like Ok(TimeFormat::FullISO));
|
||||
|
||||
// Overriding
|
||||
test!(actually: TimeFormat <- ["--time-style=default", "--time-style", "iso"], None; Last => like Ok(TimeFormat::ISOFormat(_)));
|
||||
test!(actual_2: TimeFormat <- ["--time-style=default", "--time-style", "iso"], None; Complain => err Misfire::Duplicate(Flag::Long("time-style"), Flag::Long("time-style")));
|
||||
|
||||
test!(nevermind: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"], None; Last => like Ok(TimeFormat::FullISO));
|
||||
test!(nevermore: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"], None; Complain => err Misfire::Duplicate(Flag::Long("time-style"), Flag::Long("time-style")));
|
||||
|
||||
// Errors
|
||||
test!(daily: TimeFormat <- ["--time-style=24-hour"], None; Both => err Misfire::BadArgument(&flags::TIME_STYLE, OsString::from("24-hour")));
|
||||
|
||||
// `TIME_STYLE` environment variable is defined.
|
||||
// If the time-style argument is not given, `TIME_STYLE` is used.
|
||||
test!(use_env: TimeFormat <- [], Some("long-iso".into()); Both => like Ok(TimeFormat::LongISO));
|
||||
|
||||
// If the time-style argument is given, `TIME_STYLE` is overriding.
|
||||
test!(override_env: TimeFormat <- ["--time-style=full-iso"], Some("long-iso".into()); Both => like Ok(TimeFormat::FullISO));
|
||||
}
|
||||
|
||||
|
||||
mod time_types {
|
||||
use super::*;
|
||||
|
||||
// Default behaviour
|
||||
test!(empty: TimeTypes <- []; Both => Ok(TimeTypes::default()));
|
||||
|
||||
// Modified
|
||||
test!(modified: TimeTypes <- ["--modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false }));
|
||||
test!(m: TimeTypes <- ["-m"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false }));
|
||||
test!(time_mod: TimeTypes <- ["--time=modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false }));
|
||||
test!(t_m: TimeTypes <- ["-tmod"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false }));
|
||||
|
||||
// Changed
|
||||
#[cfg(target_family = "unix")]
|
||||
test!(changed: TimeTypes <- ["--changed"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false }));
|
||||
#[cfg(target_family = "unix")]
|
||||
test!(time_ch: TimeTypes <- ["--time=changed"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false }));
|
||||
#[cfg(target_family = "unix")]
|
||||
test!(t_ch: TimeTypes <- ["-t", "ch"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false }));
|
||||
|
||||
// Accessed
|
||||
test!(acc: TimeTypes <- ["--accessed"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false }));
|
||||
test!(a: TimeTypes <- ["-u"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false }));
|
||||
test!(time_acc: TimeTypes <- ["--time", "accessed"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false }));
|
||||
test!(time_a: TimeTypes <- ["-t", "acc"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false }));
|
||||
|
||||
// Created
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
test!(cr: TimeTypes <- ["--created"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true }));
|
||||
#[cfg(target_os = "linux")]
|
||||
test!(cr: TimeTypes <- ["--created"]; Both => err Misfire::Unsupported("creation time is not available on this platform currently".to_string()));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
test!(c: TimeTypes <- ["-U"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true }));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
test!(time_cr: TimeTypes <- ["--time=created"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true }));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
test!(t_cr: TimeTypes <- ["-tcr"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true }));
|
||||
|
||||
// Multiples
|
||||
test!(time_uu: TimeTypes <- ["-u", "--modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: true, created: false }));
|
||||
|
||||
|
||||
// Errors
|
||||
test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("tea")));
|
||||
test!(t_ea: TimeTypes <- ["-tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("ea")));
|
||||
|
||||
// Overriding
|
||||
test!(overridden: TimeTypes <- ["-tcr", "-tmod"]; Last => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false }));
|
||||
test!(overridden_2: TimeTypes <- ["-tcr", "-tmod"]; Complain => err Misfire::Duplicate(Flag::Short(b't'), Flag::Short(b't')));
|
||||
}
|
||||
|
||||
|
||||
mod views {
|
||||
use super::*;
|
||||
use output::grid::Options as GridOptions;
|
||||
use output::lines::Options as LineOptions;
|
||||
|
||||
// Default
|
||||
test!(empty: Mode <- [], None; Both => like Ok(Mode::Grid(_)));
|
||||
|
||||
// Grid views
|
||||
test!(original_g: Mode <- ["-G"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, console_width: _, icons: _ })));
|
||||
test!(grid: Mode <- ["--grid"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, console_width: _, icons: _ })));
|
||||
test!(across: Mode <- ["--across"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, console_width: _, icons: _ })));
|
||||
test!(gracross: Mode <- ["-xG"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, console_width: _, icons: _ })));
|
||||
test!(icons: Mode <- ["--icons"], None; Both => like Ok(Mode::Grid(GridOptions { across: _, console_width: _, icons: true})));
|
||||
|
||||
// Lines views
|
||||
test!(lines: Mode <- ["--oneline"], None; Both => like Ok(Mode::Lines(LineOptions{ icons: _ })));
|
||||
test!(prima: Mode <- ["-1"], None; Both => like Ok(Mode::Lines(LineOptions{ icons: _ })));
|
||||
test!(line_icon: Mode <- ["-1", "--icons"], None; Both => like Ok(Mode::Lines(LineOptions { icons: true })));
|
||||
|
||||
// Details views
|
||||
test!(long: Mode <- ["--long"], None; Both => like Ok(Mode::Details(_)));
|
||||
test!(ell: Mode <- ["-l"], None; Both => like Ok(Mode::Details(_)));
|
||||
|
||||
// Grid-details views
|
||||
test!(lid: Mode <- ["--long", "--grid"], None; Both => like Ok(Mode::GridDetails(_)));
|
||||
test!(leg: Mode <- ["-lG"], None; Both => like Ok(Mode::GridDetails(_)));
|
||||
|
||||
|
||||
// Options that do nothing without --long
|
||||
test!(just_header: Mode <- ["--header"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_group: Mode <- ["--group"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_inode: Mode <- ["--inode"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_links: Mode <- ["--links"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_blocks: Mode <- ["--blocks"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_binary: Mode <- ["--binary"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_bytes: Mode <- ["--bytes"], None; Last => like Ok(Mode::Grid(_)));
|
||||
|
||||
#[cfg(feature="git")]
|
||||
test!(just_git: Mode <- ["--git"], None; Last => like Ok(Mode::Grid(_)));
|
||||
|
||||
test!(just_header_2: Mode <- ["--header"], None; Complain => err Misfire::Useless(&flags::HEADER, false, &flags::LONG));
|
||||
test!(just_group_2: Mode <- ["--group"], None; Complain => err Misfire::Useless(&flags::GROUP, false, &flags::LONG));
|
||||
test!(just_inode_2: Mode <- ["--inode"], None; Complain => err Misfire::Useless(&flags::INODE, false, &flags::LONG));
|
||||
test!(just_links_2: Mode <- ["--links"], None; Complain => err Misfire::Useless(&flags::LINKS, false, &flags::LONG));
|
||||
test!(just_blocks_2: Mode <- ["--blocks"], None; Complain => err Misfire::Useless(&flags::BLOCKS, false, &flags::LONG));
|
||||
test!(just_binary_2: Mode <- ["--binary"], None; Complain => err Misfire::Useless(&flags::BINARY, false, &flags::LONG));
|
||||
test!(just_bytes_2: Mode <- ["--bytes"], None; Complain => err Misfire::Useless(&flags::BYTES, false, &flags::LONG));
|
||||
|
||||
#[cfg(feature="git")]
|
||||
test!(just_git_2: Mode <- ["--git"], None; Complain => err Misfire::Useless(&flags::GIT, false, &flags::LONG));
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ impl TextCell {
|
||||
|
||||
TextCell {
|
||||
contents: vec![ style.paint(text) ].into(),
|
||||
width: width,
|
||||
width,
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ impl TextCell {
|
||||
|
||||
TextCell {
|
||||
contents: vec![ style.paint(text) ].into(),
|
||||
width: width,
|
||||
width,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,228 +0,0 @@
|
||||
use ansi_term::Style;
|
||||
use ansi_term::Colour::{Red, Green, Yellow, Blue, Cyan, Purple, Fixed};
|
||||
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Colours {
|
||||
pub scale: bool,
|
||||
|
||||
pub filetypes: FileTypes,
|
||||
pub perms: Permissions,
|
||||
pub size: Size,
|
||||
pub users: Users,
|
||||
pub links: Links,
|
||||
pub git: Git,
|
||||
|
||||
pub punctuation: Style,
|
||||
pub date: Style,
|
||||
pub inode: Style,
|
||||
pub blocks: Style,
|
||||
pub header: Style,
|
||||
|
||||
pub symlink_path: Style,
|
||||
pub broken_arrow: Style,
|
||||
pub broken_filename: Style,
|
||||
pub control_char: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct FileTypes {
|
||||
pub normal: Style,
|
||||
pub directory: Style,
|
||||
pub symlink: Style,
|
||||
pub pipe: Style,
|
||||
pub device: Style,
|
||||
pub socket: Style,
|
||||
pub special: Style,
|
||||
pub executable: Style,
|
||||
pub image: Style,
|
||||
pub video: Style,
|
||||
pub music: Style,
|
||||
pub lossless: Style,
|
||||
pub crypto: Style,
|
||||
pub document: Style,
|
||||
pub compressed: Style,
|
||||
pub temp: Style,
|
||||
pub immediate: Style,
|
||||
pub compiled: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Permissions {
|
||||
pub user_read: Style,
|
||||
pub user_write: Style,
|
||||
pub user_execute_file: Style,
|
||||
pub user_execute_other: Style,
|
||||
|
||||
pub group_read: Style,
|
||||
pub group_write: Style,
|
||||
pub group_execute: Style,
|
||||
|
||||
pub other_read: Style,
|
||||
pub other_write: Style,
|
||||
pub other_execute: Style,
|
||||
|
||||
pub special_user_file: Style,
|
||||
pub special_other: Style,
|
||||
|
||||
pub attribute: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Size {
|
||||
pub numbers: Style,
|
||||
pub unit: Style,
|
||||
|
||||
pub major: Style,
|
||||
pub minor: Style,
|
||||
|
||||
pub scale_byte: Style,
|
||||
pub scale_kilo: Style,
|
||||
pub scale_mega: Style,
|
||||
pub scale_giga: Style,
|
||||
pub scale_huge: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Users {
|
||||
pub user_you: Style,
|
||||
pub user_someone_else: Style,
|
||||
pub group_yours: Style,
|
||||
pub group_not_yours: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Links {
|
||||
pub normal: Style,
|
||||
pub multi_link_file: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Git {
|
||||
pub new: Style,
|
||||
pub modified: Style,
|
||||
pub deleted: Style,
|
||||
pub renamed: Style,
|
||||
pub typechange: Style,
|
||||
}
|
||||
|
||||
impl Colours {
|
||||
pub fn plain() -> Colours {
|
||||
Colours::default()
|
||||
}
|
||||
|
||||
pub fn colourful(scale: bool) -> Colours {
|
||||
Colours {
|
||||
scale: scale,
|
||||
|
||||
filetypes: FileTypes {
|
||||
normal: Style::default(),
|
||||
directory: Blue.bold(),
|
||||
symlink: Cyan.normal(),
|
||||
pipe: Yellow.normal(),
|
||||
device: Yellow.bold(),
|
||||
socket: Red.bold(),
|
||||
special: Yellow.normal(),
|
||||
executable: Green.bold(),
|
||||
image: Fixed(133).normal(),
|
||||
video: Fixed(135).normal(),
|
||||
music: Fixed(92).normal(),
|
||||
lossless: Fixed(93).normal(),
|
||||
crypto: Fixed(109).normal(),
|
||||
document: Fixed(105).normal(),
|
||||
compressed: Red.normal(),
|
||||
temp: Fixed(244).normal(),
|
||||
immediate: Yellow.bold().underline(),
|
||||
compiled: Fixed(137).normal(),
|
||||
},
|
||||
|
||||
perms: Permissions {
|
||||
user_read: Yellow.bold(),
|
||||
user_write: Red.bold(),
|
||||
user_execute_file: Green.bold().underline(),
|
||||
user_execute_other: Green.bold(),
|
||||
|
||||
group_read: Yellow.normal(),
|
||||
group_write: Red.normal(),
|
||||
group_execute: Green.normal(),
|
||||
|
||||
other_read: Yellow.normal(),
|
||||
other_write: Red.normal(),
|
||||
other_execute: Green.normal(),
|
||||
|
||||
special_user_file: Purple.normal(),
|
||||
special_other: Purple.normal(),
|
||||
|
||||
attribute: Style::default(),
|
||||
},
|
||||
|
||||
size: Size {
|
||||
numbers: Green.bold(),
|
||||
unit: Green.normal(),
|
||||
|
||||
major: Green.bold(),
|
||||
minor: Green.normal(),
|
||||
|
||||
scale_byte: Fixed(118).normal(),
|
||||
scale_kilo: Fixed(190).normal(),
|
||||
scale_mega: Fixed(226).normal(),
|
||||
scale_giga: Fixed(220).normal(),
|
||||
scale_huge: Fixed(214).normal(),
|
||||
},
|
||||
|
||||
users: Users {
|
||||
user_you: Yellow.bold(),
|
||||
user_someone_else: Style::default(),
|
||||
group_yours: Yellow.bold(),
|
||||
group_not_yours: Style::default(),
|
||||
},
|
||||
|
||||
links: Links {
|
||||
normal: Red.bold(),
|
||||
multi_link_file: Red.on(Yellow),
|
||||
},
|
||||
|
||||
git: Git {
|
||||
new: Green.normal(),
|
||||
modified: Blue.normal(),
|
||||
deleted: Red.normal(),
|
||||
renamed: Yellow.normal(),
|
||||
typechange: Purple.normal(),
|
||||
},
|
||||
|
||||
punctuation: Fixed(244).normal(),
|
||||
date: Blue.normal(),
|
||||
inode: Purple.normal(),
|
||||
blocks: Cyan.normal(),
|
||||
header: Style::default().underline(),
|
||||
|
||||
symlink_path: Cyan.normal(),
|
||||
broken_arrow: Red.normal(),
|
||||
broken_filename: Red.underline(),
|
||||
control_char: Red.normal(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_size(&self, size: u64) -> Style {
|
||||
if self.scale {
|
||||
if size < 1024 {
|
||||
self.size.scale_byte
|
||||
}
|
||||
else if size < 1024 * 1024 {
|
||||
self.size.scale_kilo
|
||||
}
|
||||
else if size < 1024 * 1024 * 1024 {
|
||||
self.size.scale_mega
|
||||
}
|
||||
else if size < 1024 * 1024 * 1024 * 1024 {
|
||||
self.size.scale_giga
|
||||
}
|
||||
else {
|
||||
self.size.scale_huge
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.size.numbers
|
||||
}
|
||||
}
|
||||
}
|
@ -64,14 +64,21 @@ use std::io::{Write, Error as IOError, Result as IOResult};
|
||||
use std::path::PathBuf;
|
||||
use std::vec::IntoIter as VecIntoIter;
|
||||
|
||||
use ansi_term::{ANSIGenericString, Style};
|
||||
|
||||
use fs::{Dir, File};
|
||||
use fs::dir_action::RecurseOptions;
|
||||
use fs::filter::FileFilter;
|
||||
use fs::feature::ignore::IgnoreCache;
|
||||
use fs::feature::git::GitCache;
|
||||
use fs::feature::xattr::{Attribute, FileAttributes};
|
||||
use options::{FileFilter, RecurseOptions};
|
||||
use output::colours::Colours;
|
||||
use style::Colours;
|
||||
use output::cell::TextCell;
|
||||
use output::tree::{TreeTrunk, TreeParams, TreeDepth};
|
||||
use output::file_name::FileStyle;
|
||||
use output::table::{Table, Options as TableOptions, Row as TableRow};
|
||||
use output::icons::painted_icon;
|
||||
use scoped_threadpool::Pool;
|
||||
|
||||
|
||||
/// With the **Details** view, the output gets formatted into columns, with
|
||||
@ -99,6 +106,9 @@ pub struct Options {
|
||||
|
||||
/// Whether to show each file's extended attributes.
|
||||
pub xattr: bool,
|
||||
|
||||
/// Enables --icons mode
|
||||
pub icons: bool,
|
||||
}
|
||||
|
||||
|
||||
@ -126,6 +136,7 @@ struct Egg<'a> {
|
||||
errors: Vec<(IOError, Option<PathBuf>)>,
|
||||
dir: Option<Dir>,
|
||||
file: &'a File<'a>,
|
||||
icon: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> AsRef<File<'a>> for Egg<'a> {
|
||||
@ -136,11 +147,18 @@ impl<'a> AsRef<File<'a>> for Egg<'a> {
|
||||
|
||||
|
||||
impl<'a> Render<'a> {
|
||||
pub fn render<W: Write>(self, w: &mut W) -> IOResult<()> {
|
||||
pub fn render<W: Write>(self, mut git: Option<&'a GitCache>, ignore: Option<&'a IgnoreCache>, w: &mut W) -> IOResult<()> {
|
||||
let mut pool = Pool::new(num_cpus::get() as u32);
|
||||
let mut rows = Vec::new();
|
||||
|
||||
if let Some(ref table) = self.opts.table {
|
||||
let mut table = Table::new(&table, self.dir, &self.colours);
|
||||
match (git, self.dir) {
|
||||
(Some(g), Some(d)) => if !g.has_anything_for(&d.path) { git = None },
|
||||
(Some(g), None) => if !self.files.iter().any(|f| g.has_anything_for(&f.path)) { git = None },
|
||||
(None, _) => {/* Keep Git how it is */},
|
||||
}
|
||||
|
||||
let mut table = Table::new(&table, git, &self.colours);
|
||||
|
||||
if self.opts.header {
|
||||
let header = table.header_row();
|
||||
@ -148,17 +166,17 @@ impl<'a> Render<'a> {
|
||||
rows.push(self.render_header(header));
|
||||
}
|
||||
|
||||
// This is weird, but I can't find a way around it:
|
||||
// This is weird, but I can’t find a way around it:
|
||||
// https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6
|
||||
let mut table = Some(table);
|
||||
self.add_files_to_table(&mut table, &mut rows, &self.files, TreeDepth::root());
|
||||
self.add_files_to_table(&mut pool, &mut table, &mut rows, &self.files, ignore, TreeDepth::root());
|
||||
|
||||
for row in self.iterate_with_table(table.unwrap(), rows) {
|
||||
writeln!(w, "{}", row.strings())?
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.add_files_to_table(&mut None, &mut rows, &self.files, TreeDepth::root());
|
||||
self.add_files_to_table(&mut pool, &mut None, &mut rows, &self.files, ignore, TreeDepth::root());
|
||||
|
||||
for row in self.iterate(rows) {
|
||||
writeln!(w, "{}", row.strings())?
|
||||
@ -170,13 +188,10 @@ impl<'a> Render<'a> {
|
||||
|
||||
/// Adds files to the table, possibly recursively. This is easily
|
||||
/// parallelisable, and uses a pool of threads.
|
||||
fn add_files_to_table<'dir>(&self, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &Vec<File<'dir>>, depth: TreeDepth) {
|
||||
use num_cpus;
|
||||
use scoped_threadpool::Pool;
|
||||
fn add_files_to_table<'dir, 'ig>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], ignore: Option<&'ig IgnoreCache>, depth: TreeDepth) {
|
||||
use std::sync::{Arc, Mutex};
|
||||
use fs::feature::xattr;
|
||||
|
||||
let mut pool = Pool::new(num_cpus::get() as u32);
|
||||
let mut file_eggs = Vec::new();
|
||||
|
||||
pool.scoped(|scoped| {
|
||||
@ -184,17 +199,48 @@ impl<'a> Render<'a> {
|
||||
let table = table.as_ref();
|
||||
|
||||
for file in src {
|
||||
let file_eggs = file_eggs.clone();
|
||||
let file_eggs = Arc::clone(&file_eggs);
|
||||
|
||||
scoped.execute(move || {
|
||||
let mut errors = Vec::new();
|
||||
let mut xattrs = Vec::new();
|
||||
|
||||
// There are three “levels” of extended attribute support:
|
||||
//
|
||||
// 1. If we’re compiling without that feature, then
|
||||
// exa pretends all files have no attributes.
|
||||
// 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
|
||||
// errors encountered when getting them.
|
||||
// 3. If the --extended flag *hasn’t* been specified, then
|
||||
// display the @, but don’t display anything else.
|
||||
//
|
||||
// For a while, exa took a stricter approach to (3):
|
||||
// if an error occurred while checking a file’s xattrs to
|
||||
// see if it should display the @, exa would display that
|
||||
// error even though the attributes weren’t actually being
|
||||
// shown! This was confusing, as users were being shown
|
||||
// errors for something they didn’t explicitly ask for,
|
||||
// and just cluttered up the output. So now errors aren’t
|
||||
// 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) => errors.push((e, None)),
|
||||
};
|
||||
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 table_row = table.as_ref().map(|t| t.row_for_file(&file, !xattrs.is_empty()));
|
||||
@ -207,14 +253,18 @@ impl<'a> Render<'a> {
|
||||
|
||||
if let Some(r) = self.recurse {
|
||||
if file.is_directory() && r.tree && !r.is_too_deep(depth.0) {
|
||||
match file.to_dir(false) {
|
||||
match file.to_dir() {
|
||||
Ok(d) => { dir = Some(d); },
|
||||
Err(e) => { errors.push((e, None)) },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let egg = Egg { table_row, xattrs, errors, dir, file };
|
||||
let icon = if self.opts.icons {
|
||||
Some(painted_icon(&file, &self.style))
|
||||
} else { None };
|
||||
|
||||
let egg = Egg { table_row, xattrs, errors, dir, file, icon };
|
||||
file_eggs.lock().unwrap().push(egg);
|
||||
});
|
||||
}
|
||||
@ -230,18 +280,26 @@ impl<'a> Render<'a> {
|
||||
t.add_widths(row);
|
||||
}
|
||||
|
||||
let mut name_cell = TextCell::default();
|
||||
if let Some(icon) = egg.icon {
|
||||
name_cell.push(ANSIGenericString::from(icon), 2)
|
||||
}
|
||||
name_cell.append(self.style.for_file(&egg.file, self.colours)
|
||||
.with_link_paths()
|
||||
.paint()
|
||||
.promote());
|
||||
|
||||
|
||||
let row = Row {
|
||||
tree: tree_params,
|
||||
cells: egg.table_row,
|
||||
name: self.style.for_file(&egg.file, self.colours)
|
||||
.with_link_paths()
|
||||
.paint().promote(),
|
||||
name: name_cell,
|
||||
};
|
||||
|
||||
rows.push(row);
|
||||
|
||||
if let Some(ref dir) = egg.dir {
|
||||
for file_to_add in dir.files(self.filter.dot_filter) {
|
||||
for file_to_add in dir.files(self.filter.dot_filter, ignore) {
|
||||
match file_to_add {
|
||||
Ok(f) => files.push(f),
|
||||
Err((path, e)) => errors.push((e, Some(path)))
|
||||
@ -252,21 +310,21 @@ 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 {
|
||||
rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), false), path));
|
||||
}
|
||||
|
||||
self.add_files_to_table(table, rows, &files, depth.deeper());
|
||||
self.add_files_to_table(pool, table, rows, &files, ignore, depth.deeper());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let count = egg.xattrs.len();
|
||||
for (index, xattr) in egg.xattrs.into_iter().enumerate() {
|
||||
rows.push(self.render_xattr(xattr, TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1)));
|
||||
rows.push(self.render_xattr(&xattr, TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1)));
|
||||
}
|
||||
|
||||
let count = errors.len();
|
||||
@ -285,16 +343,20 @@ impl<'a> Render<'a> {
|
||||
}
|
||||
|
||||
fn render_error(&self, error: &IOError, tree: TreeParams, path: Option<PathBuf>) -> Row {
|
||||
use output::file_name::Colours;
|
||||
|
||||
let error_message = match path {
|
||||
Some(path) => format!("<{}: {}>", path.display(), error),
|
||||
None => format!("<{}>", error),
|
||||
};
|
||||
|
||||
let name = TextCell::paint(self.colours.broken_arrow, error_message);
|
||||
// TODO: broken_symlink() doesn’t quite seem like the right name for
|
||||
// the style that’s being used here. Maybe split it in two?
|
||||
let name = TextCell::paint(self.colours.broken_symlink(), error_message);
|
||||
Row { cells: None, name, tree }
|
||||
}
|
||||
|
||||
fn render_xattr(&self, xattr: Attribute, tree: TreeParams) -> Row {
|
||||
fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
|
||||
let name = TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size));
|
||||
Row { cells: None, name, tree }
|
||||
}
|
||||
@ -307,17 +369,17 @@ impl<'a> Render<'a> {
|
||||
TableIter {
|
||||
tree_trunk: TreeTrunk::default(),
|
||||
total_width: table.widths().total(),
|
||||
table: table,
|
||||
table,
|
||||
inner: rows.into_iter(),
|
||||
colours: self.colours,
|
||||
tree_style: self.colours.punctuation,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iterate(&'a self, rows: Vec<Row>) -> Iter<'a> {
|
||||
pub fn iterate(&'a self, rows: Vec<Row>) -> Iter {
|
||||
Iter {
|
||||
tree_trunk: TreeTrunk::default(),
|
||||
inner: rows.into_iter(),
|
||||
colours: self.colours,
|
||||
tree_style: self.colours.punctuation,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -343,11 +405,12 @@ pub struct Row {
|
||||
|
||||
|
||||
pub struct TableIter<'a> {
|
||||
table: Table<'a>,
|
||||
tree_trunk: TreeTrunk,
|
||||
total_width: usize,
|
||||
colours: &'a Colours,
|
||||
inner: VecIntoIter<Row>,
|
||||
table: Table<'a>,
|
||||
|
||||
total_width: usize,
|
||||
tree_style: Style,
|
||||
tree_trunk: TreeTrunk,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TableIter<'a> {
|
||||
@ -366,7 +429,7 @@ impl<'a> Iterator for TableIter<'a> {
|
||||
};
|
||||
|
||||
for tree_part in self.tree_trunk.new_row(row.tree) {
|
||||
cell.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
|
||||
cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
|
||||
}
|
||||
|
||||
// If any tree characters have been printed, then add an extra
|
||||
@ -382,13 +445,13 @@ impl<'a> Iterator for TableIter<'a> {
|
||||
}
|
||||
|
||||
|
||||
pub struct Iter<'a> {
|
||||
pub struct Iter {
|
||||
tree_trunk: TreeTrunk,
|
||||
colours: &'a Colours,
|
||||
tree_style: Style,
|
||||
inner: VecIntoIter<Row>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
impl Iterator for Iter {
|
||||
type Item = TextCell;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
@ -396,7 +459,7 @@ impl<'a> Iterator for Iter<'a> {
|
||||
let mut cell = TextCell::default();
|
||||
|
||||
for tree_part in self.tree_trunk.new_row(row.tree) {
|
||||
cell.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
|
||||
cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
|
||||
}
|
||||
|
||||
// If any tree characters have been printed, then add an extra
|
||||
|
@ -3,10 +3,9 @@ use std::path::Path;
|
||||
use ansi_term::{ANSIString, Style};
|
||||
|
||||
use fs::{File, FileTarget};
|
||||
use info::filetype::FileExtensions;
|
||||
use output::Colours;
|
||||
use output::escape;
|
||||
use output::cell::TextCellContents;
|
||||
use output::render::FiletypeColours;
|
||||
|
||||
|
||||
/// Basically a file name factory.
|
||||
@ -17,19 +16,19 @@ pub struct FileStyle {
|
||||
pub classify: Classify,
|
||||
|
||||
/// Mapping of file extensions to colours, to highlight regular files.
|
||||
pub exts: FileExtensions,
|
||||
pub exts: Box<FileColours>,
|
||||
}
|
||||
|
||||
impl FileStyle {
|
||||
|
||||
/// Create a new `FileName` that prints the given file’s name, painting it
|
||||
/// with the remaining arguments.
|
||||
pub fn for_file<'a, 'dir>(&'a self, file: &'a File<'dir>, colours: &'a Colours) -> FileName<'a, 'dir> {
|
||||
pub fn for_file<'a, 'dir, C: Colours>(&'a self, file: &'a File<'dir>, colours: &'a C) -> FileName<'a, 'dir, C> {
|
||||
FileName {
|
||||
file, colours,
|
||||
link_style: LinkStyle::JustFilenames,
|
||||
exts: &self.exts,
|
||||
classify: self.classify,
|
||||
exts: &*self.exts,
|
||||
target: if file.is_link() { Some(file.link_target()) }
|
||||
else { None }
|
||||
}
|
||||
@ -75,15 +74,15 @@ impl Default for Classify {
|
||||
|
||||
/// A **file name** holds all the information necessary to display the name
|
||||
/// of the given file. This is used in all of the views.
|
||||
pub struct FileName<'a, 'dir: 'a> {
|
||||
pub struct FileName<'a, 'dir: 'a, C: Colours+'a> {
|
||||
|
||||
/// A reference to the file that we're getting the name of.
|
||||
/// A reference to the file that we’re getting the name of.
|
||||
file: &'a File<'dir>,
|
||||
|
||||
/// The colours used to paint the file name and its surrounding text.
|
||||
colours: &'a Colours,
|
||||
colours: &'a C,
|
||||
|
||||
/// The file that this file points to if it's a link.
|
||||
/// The file that this file points to if it’s a link.
|
||||
target: Option<FileTarget<'dir>>,
|
||||
|
||||
/// How to handle displaying links.
|
||||
@ -93,11 +92,11 @@ pub struct FileName<'a, 'dir: 'a> {
|
||||
classify: Classify,
|
||||
|
||||
/// Mapping of file extensions to colours, to highlight regular files.
|
||||
exts: &'a FileExtensions,
|
||||
exts: &'a FileColours,
|
||||
}
|
||||
|
||||
|
||||
impl<'a, 'dir> FileName<'a, 'dir> {
|
||||
impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
||||
|
||||
/// Sets the flag on this file name to display link targets with an
|
||||
/// arrow followed by their path.
|
||||
@ -122,6 +121,12 @@ impl<'a, 'dir> FileName<'a, 'dir> {
|
||||
}
|
||||
|
||||
if !self.file.name.is_empty() {
|
||||
// The “missing file” colour seems like it should be used here,
|
||||
// but it’s not! In a grid view, where there's no space to display
|
||||
// link targets, the filename has to have a different style to
|
||||
// indicate this fact. But when showing targets, we can just
|
||||
// colour the path instead (see below), and leave the broken
|
||||
// link’s filename as the link colour.
|
||||
for bit in self.coloured_file_name() {
|
||||
bits.push(bit);
|
||||
}
|
||||
@ -131,7 +136,7 @@ impl<'a, 'dir> FileName<'a, 'dir> {
|
||||
match *target {
|
||||
FileTarget::Ok(ref target) => {
|
||||
bits.push(Style::default().paint(" "));
|
||||
bits.push(self.colours.punctuation.paint("->"));
|
||||
bits.push(self.colours.normal_arrow().paint("->"));
|
||||
bits.push(Style::default().paint(" "));
|
||||
|
||||
if let Some(parent) = target.path.parent() {
|
||||
@ -156,9 +161,9 @@ impl<'a, 'dir> FileName<'a, 'dir> {
|
||||
|
||||
FileTarget::Broken(ref broken_path) => {
|
||||
bits.push(Style::default().paint(" "));
|
||||
bits.push(self.colours.broken_arrow.paint("->"));
|
||||
bits.push(self.colours.broken_symlink().paint("->"));
|
||||
bits.push(Style::default().paint(" "));
|
||||
escape(broken_path.display().to_string(), &mut bits, self.colours.broken_filename, self.colours.control_char.underline());
|
||||
escape(broken_path.display().to_string(), &mut bits, self.colours.broken_filename(), self.colours.broken_control_char());
|
||||
},
|
||||
|
||||
FileTarget::Err(_) => {
|
||||
@ -182,11 +187,11 @@ impl<'a, 'dir> FileName<'a, 'dir> {
|
||||
let coconut = parent.components().count();
|
||||
|
||||
if coconut == 1 && parent.has_root() {
|
||||
bits.push(self.colours.symlink_path.paint("/"));
|
||||
bits.push(self.colours.symlink_path().paint("/"));
|
||||
}
|
||||
else if coconut >= 1 {
|
||||
escape(parent.to_string_lossy().to_string(), bits, self.colours.symlink_path, self.colours.control_char);
|
||||
bits.push(self.colours.symlink_path.paint("/"));
|
||||
escape(parent.to_string_lossy().to_string(), bits, self.colours.symlink_path(), self.colours.control_char());
|
||||
bits.push(self.colours.symlink_path().paint("/"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,50 +228,96 @@ impl<'a, 'dir> FileName<'a, 'dir> {
|
||||
fn coloured_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
|
||||
let file_style = self.style();
|
||||
let mut bits = Vec::new();
|
||||
escape(self.file.name.clone(), &mut bits, file_style, self.colours.control_char);
|
||||
escape(self.file.name.clone(), &mut bits, file_style, self.colours.control_char());
|
||||
bits
|
||||
}
|
||||
|
||||
|
||||
/// 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.
|
||||
/// class on the filesystem or from its name. (Or the broken link colour,
|
||||
/// if there’s nowhere else for that fact to be shown.)
|
||||
pub fn style(&self) -> Style {
|
||||
|
||||
// Override the style with the “broken link” style when this file is
|
||||
// a link that we can’t follow for whatever reason. This is used when
|
||||
// there’s no other place to show that the link doesn’t work.
|
||||
if let LinkStyle::JustFilenames = self.link_style {
|
||||
if let Some(ref target) = self.target {
|
||||
if target.is_broken() {
|
||||
return self.colours.broken_arrow;
|
||||
return self.colours.broken_symlink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, just apply a bunch of rules in order. For example,
|
||||
// executable image files should be executable rather than images.
|
||||
match self.file {
|
||||
f if f.is_directory() => self.colours.filetypes.directory,
|
||||
f if f.is_executable_file() => self.colours.filetypes.executable,
|
||||
f if f.is_link() => self.colours.filetypes.symlink,
|
||||
f if f.is_pipe() => self.colours.filetypes.pipe,
|
||||
f if f.is_char_device()
|
||||
| f.is_block_device() => self.colours.filetypes.device,
|
||||
f if f.is_socket() => self.colours.filetypes.socket,
|
||||
f if !f.is_file() => self.colours.filetypes.special,
|
||||
self.kind_style()
|
||||
.or_else(|| self.exts.colour_file(self.file))
|
||||
.unwrap_or_else(|| self.colours.normal())
|
||||
}
|
||||
|
||||
f if self.exts.is_immediate(f) => self.colours.filetypes.immediate,
|
||||
f if self.exts.is_image(f) => self.colours.filetypes.image,
|
||||
f if self.exts.is_video(f) => self.colours.filetypes.video,
|
||||
f if self.exts.is_music(f) => self.colours.filetypes.music,
|
||||
f if self.exts.is_lossless(f) => self.colours.filetypes.lossless,
|
||||
f if self.exts.is_crypto(f) => self.colours.filetypes.crypto,
|
||||
f if self.exts.is_document(f) => self.colours.filetypes.document,
|
||||
f if self.exts.is_compressed(f) => self.colours.filetypes.compressed,
|
||||
f if self.exts.is_temp(f) => self.colours.filetypes.temp,
|
||||
f if self.exts.is_compiled(f) => self.colours.filetypes.compiled,
|
||||
_ => self.colours.filetypes.normal,
|
||||
}
|
||||
fn kind_style(&self) -> Option<Style> {
|
||||
Some(match self.file {
|
||||
f if f.is_directory() => self.colours.directory(),
|
||||
f if f.is_executable_file() => self.colours.executable_file(),
|
||||
f if f.is_link() => self.colours.symlink(),
|
||||
f if f.is_pipe() => self.colours.pipe(),
|
||||
f if f.is_block_device() => self.colours.block_device(),
|
||||
f if f.is_char_device() => self.colours.char_device(),
|
||||
f if f.is_socket() => self.colours.socket(),
|
||||
f if !f.is_file() => self.colours.special(),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The set of colours that are needed to paint a file name.
|
||||
pub trait Colours: FiletypeColours {
|
||||
|
||||
/// The style to paint the path of a symlink’s target, up to but not
|
||||
/// including the file’s name.
|
||||
fn symlink_path(&self) -> Style;
|
||||
|
||||
/// The style to paint the arrow between a link and its target.
|
||||
fn normal_arrow(&self) -> Style;
|
||||
|
||||
/// The style to paint the filenames of broken links in views that don’t
|
||||
/// show link targets, and the style to paint the *arrow* between the link
|
||||
/// and its target in views that *do* show link targets.
|
||||
fn broken_symlink(&self) -> Style;
|
||||
|
||||
/// The style to paint the entire filename of a broken link.
|
||||
fn broken_filename(&self) -> Style;
|
||||
|
||||
/// The style to paint a non-displayable control character in a filename.
|
||||
fn control_char(&self) -> Style;
|
||||
|
||||
/// The style to paint a non-displayable control character in a filename,
|
||||
/// when the filename is being displayed as a broken link target.
|
||||
fn broken_control_char(&self) -> Style;
|
||||
|
||||
/// The style to paint a file that has its executable bit set.
|
||||
fn executable_file(&self) -> Style;
|
||||
}
|
||||
|
||||
|
||||
// needs Debug because FileStyle derives it
|
||||
use std::fmt::Debug;
|
||||
use std::marker::Sync;
|
||||
pub trait FileColours: Debug+Sync {
|
||||
fn colour_file(&self, file: &File) -> Option<Style>;
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct NoFileColours;
|
||||
impl FileColours for NoFileColours {
|
||||
fn colour_file(&self, _file: &File) -> Option<Style> { None }
|
||||
}
|
||||
|
||||
// When getting the colour of a file from a *pair* of colourisers, try the
|
||||
// first one then try the second one. This lets the user provide their own
|
||||
// file type associations, while falling back to the default set if not set
|
||||
// explicitly.
|
||||
impl<A, B> FileColours for (A, B)
|
||||
where A: FileColours, B: FileColours {
|
||||
fn colour_file(&self, file: &File) -> Option<Style> {
|
||||
self.0.colour_file(file).or_else(|| self.1.colour_file(file))
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,17 @@ use std::io::{Write, Result as IOResult};
|
||||
use term_grid as tg;
|
||||
|
||||
use fs::File;
|
||||
use output::colours::Colours;
|
||||
use style::Colours;
|
||||
use output::file_name::FileStyle;
|
||||
use output::icons::painted_icon;
|
||||
use output::cell::DisplayWidth;
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct Options {
|
||||
pub across: bool,
|
||||
pub console_width: usize,
|
||||
pub icons: bool,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
@ -37,12 +40,17 @@ impl<'a> Render<'a> {
|
||||
|
||||
grid.reserve(self.files.len());
|
||||
|
||||
for file in self.files.iter() {
|
||||
for file in &self.files {
|
||||
let icon = if self.opts.icons { Some(painted_icon(&file, &self.style)) } else { None };
|
||||
let filename = self.style.for_file(file, self.colours).paint();
|
||||
let width = filename.width();
|
||||
let width = if self.opts.icons {
|
||||
DisplayWidth::from(2) + filename.width()
|
||||
} else {
|
||||
filename.width()
|
||||
};
|
||||
|
||||
grid.add(tg::Cell {
|
||||
contents: filename.strings().to_string(),
|
||||
contents: format!("{icon}{filename}", icon=&icon.unwrap_or("".to_string()), filename=filename.strings().to_string()),
|
||||
width: *width,
|
||||
});
|
||||
}
|
||||
@ -54,7 +62,7 @@ impl<'a> Render<'a> {
|
||||
// File names too long for a grid - drop down to just listing them!
|
||||
// This isn’t *quite* the same as the lines view, which also
|
||||
// displays full link paths.
|
||||
for file in self.files.iter() {
|
||||
for file in &self.files {
|
||||
let name_cell = self.style.for_file(file, self.colours).paint();
|
||||
writeln!(w, "{}", name_cell.strings())?;
|
||||
}
|
||||
|
@ -1,35 +1,93 @@
|
||||
//! The grid-details view lists several details views side-by-side.
|
||||
|
||||
use std::io::{Write, Result as IOResult};
|
||||
|
||||
use ansi_term::ANSIStrings;
|
||||
use ansi_term::{ANSIGenericString, ANSIStrings};
|
||||
use term_grid as grid;
|
||||
|
||||
use fs::{Dir, File};
|
||||
use fs::feature::git::GitCache;
|
||||
use fs::feature::xattr::FileAttributes;
|
||||
use fs::filter::FileFilter;
|
||||
|
||||
use options::FileFilter;
|
||||
use style::Colours;
|
||||
use output::cell::TextCell;
|
||||
use output::colours::Colours;
|
||||
use output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
|
||||
use output::grid::Options as GridOptions;
|
||||
use output::file_name::FileStyle;
|
||||
use output::table::{Table, Row as TableRow, Options as TableOptions};
|
||||
use output::tree::{TreeParams, TreeDepth};
|
||||
use output::icons::painted_icon;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Options {
|
||||
pub grid: GridOptions,
|
||||
pub details: DetailsOptions,
|
||||
pub row_threshold: RowThreshold,
|
||||
}
|
||||
|
||||
/// The grid-details view can be configured to revert to just a details view
|
||||
/// (with one column) if it wouldn’t produce enough rows of output.
|
||||
///
|
||||
/// Doing this makes the resulting output look a bit better: when listing a
|
||||
/// 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(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum RowThreshold {
|
||||
|
||||
/// Only use grid-details view if it would result in at least this many
|
||||
/// rows of output.
|
||||
MinimumRows(usize),
|
||||
|
||||
/// Use the grid-details view no matter what.
|
||||
AlwaysGrid,
|
||||
}
|
||||
|
||||
|
||||
pub struct Render<'a> {
|
||||
|
||||
/// The directory that’s being rendered here.
|
||||
/// We need this to know which columns to put in the output.
|
||||
pub dir: Option<&'a Dir>,
|
||||
|
||||
/// The files that have been read from the directory. They should all
|
||||
/// hold a reference to it.
|
||||
pub files: Vec<File<'a>>,
|
||||
|
||||
/// How to colour various pieces of text.
|
||||
pub colours: &'a Colours,
|
||||
|
||||
/// How to format filenames.
|
||||
pub style: &'a FileStyle,
|
||||
|
||||
/// The grid part of the grid-details view.
|
||||
pub grid: &'a GridOptions,
|
||||
|
||||
/// The details part of the grid-details view.
|
||||
pub details: &'a DetailsOptions,
|
||||
|
||||
/// How to filter files after listing a directory. The files in this
|
||||
/// render will already have been filtered and sorted, but any directories
|
||||
/// that we recurse into will have to have this applied.
|
||||
pub filter: &'a FileFilter,
|
||||
|
||||
/// The minimum number of rows that there need to be before grid-details
|
||||
/// mode is activated.
|
||||
pub row_threshold: RowThreshold,
|
||||
}
|
||||
|
||||
impl<'a> Render<'a> {
|
||||
|
||||
/// Create a temporary Details render that gets used for the columns of
|
||||
/// the grid-details render that's being generated.
|
||||
///
|
||||
/// This includes an empty files vector because the files get added to
|
||||
/// the table in *this* file, not in details: we only want to insert every
|
||||
/// *n* files into each column’s table, not all of them.
|
||||
pub fn details(&self) -> DetailsRender<'a> {
|
||||
DetailsRender {
|
||||
dir: self.dir.clone(),
|
||||
dir: self.dir,
|
||||
files: Vec::new(),
|
||||
colours: self.colours,
|
||||
style: self.style,
|
||||
@ -39,26 +97,63 @@ impl<'a> Render<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
|
||||
/// Create a Details render for when this grid-details render doesn’t fit
|
||||
/// in the terminal (or something has gone wrong) and we have given up.
|
||||
pub fn give_up(self) -> DetailsRender<'a> {
|
||||
DetailsRender {
|
||||
dir: self.dir,
|
||||
files: self.files,
|
||||
colours: self.colours,
|
||||
style: self.style,
|
||||
opts: self.details,
|
||||
recurse: None,
|
||||
filter: &self.filter,
|
||||
}
|
||||
}
|
||||
|
||||
// This doesn’t take an IgnoreCache even though the details one does
|
||||
// because grid-details has no tree view.
|
||||
|
||||
pub fn render<W: Write>(self, git: Option<&GitCache>, w: &mut W) -> IOResult<()> {
|
||||
if let Some((grid, width)) = self.find_fitting_grid(git) {
|
||||
write!(w, "{}", grid.fit_into_columns(width))
|
||||
}
|
||||
else {
|
||||
self.give_up().render(git, None, w)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_fitting_grid(&self, git: Option<&GitCache>) -> Option<(grid::Grid, grid::Width)> {
|
||||
let options = self.details.table.as_ref().expect("Details table options not given!");
|
||||
|
||||
let drender = self.clone().details();
|
||||
let drender = self.details();
|
||||
|
||||
let (first_table, _) = self.make_table(options, &drender);
|
||||
let (first_table, _) = self.make_table(options, git, &drender);
|
||||
|
||||
let rows = self.files.iter()
|
||||
.map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
|
||||
.collect::<Vec<TableRow>>();
|
||||
|
||||
let file_names = self.files.iter()
|
||||
.map(|file| self.style.for_file(file, self.colours).paint().promote())
|
||||
.map(|file| {
|
||||
if self.details.icons {
|
||||
let mut icon_cell = TextCell::default();
|
||||
icon_cell.push(ANSIGenericString::from(painted_icon(&file, &self.style)), 2);
|
||||
let file_cell = self.style.for_file(file, self.colours).paint().promote();
|
||||
icon_cell.append(file_cell);
|
||||
icon_cell
|
||||
} else {
|
||||
self.style.for_file(file, self.colours).paint().promote()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<TextCell>>();
|
||||
|
||||
let mut last_working_table = self.make_grid(1, options, &file_names, rows.clone(), &drender);
|
||||
let mut last_working_table = self.make_grid(1, options, git, &file_names, rows.clone(), &drender);
|
||||
|
||||
for column_count in 2.. {
|
||||
let grid = self.make_grid(column_count, options, &file_names, rows.clone(), &drender);
|
||||
// If we can’t fit everything in a grid 100 columns wide, then
|
||||
// something has gone seriously awry
|
||||
for column_count in 2..100 {
|
||||
let grid = self.make_grid(column_count, options, git, &file_names, rows.clone(), &drender);
|
||||
|
||||
let the_grid_fits = {
|
||||
let d = grid.fit_into_columns(column_count);
|
||||
@ -69,15 +164,30 @@ impl<'a> Render<'a> {
|
||||
last_working_table = grid;
|
||||
}
|
||||
else {
|
||||
return write!(w, "{}", last_working_table.fit_into_columns(column_count - 1));
|
||||
// If we’ve figured out how many columns can fit in the user’s
|
||||
// terminal, and it turns out there aren’t enough rows to
|
||||
// make it worthwhile, then just resort to the lines view.
|
||||
if let RowThreshold::MinimumRows(thresh) = self.row_threshold {
|
||||
if last_working_table.fit_into_columns(column_count - 1).row_count() < thresh {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
return Some((last_working_table, column_count - 1));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
None
|
||||
}
|
||||
|
||||
fn make_table<'t>(&'a self, options: &'a TableOptions, drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
|
||||
let mut table = Table::new(options, self.dir, self.colours);
|
||||
fn make_table(&'a self, options: &'a TableOptions, mut git: Option<&'a GitCache>, drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
|
||||
match (git, self.dir) {
|
||||
(Some(g), Some(d)) => if !g.has_anything_for(&d.path) { git = None },
|
||||
(Some(g), None) => if !self.files.iter().any(|f| g.has_anything_for(&f.path)) { git = None },
|
||||
(None, _) => {/* Keep Git how it is */},
|
||||
}
|
||||
|
||||
let mut table = Table::new(options, git, self.colours);
|
||||
let mut rows = Vec::new();
|
||||
|
||||
if self.details.header {
|
||||
@ -89,11 +199,11 @@ impl<'a> Render<'a> {
|
||||
(table, rows)
|
||||
}
|
||||
|
||||
fn make_grid(&'a self, column_count: usize, options: &'a TableOptions, file_names: &[TextCell], rows: Vec<TableRow>, drender: &DetailsRender) -> grid::Grid {
|
||||
fn make_grid(&'a self, column_count: usize, options: &'a TableOptions, git: Option<&GitCache>, file_names: &[TextCell], rows: Vec<TableRow>, drender: &DetailsRender) -> grid::Grid {
|
||||
|
||||
let mut tables = Vec::new();
|
||||
for _ in 0 .. column_count {
|
||||
tables.push(self.make_table(options, drender));
|
||||
tables.push(self.make_table(options, git, drender));
|
||||
}
|
||||
|
||||
let mut num_cells = rows.len();
|
||||
@ -126,7 +236,7 @@ impl<'a> Render<'a> {
|
||||
else { grid::Direction::TopToBottom };
|
||||
|
||||
let mut grid = grid::Grid::new(grid::GridOptions {
|
||||
direction: direction,
|
||||
direction,
|
||||
filling: grid::Filling::Spaces(4),
|
||||
});
|
||||
|
||||
|
120
src/output/icons.rs
Normal file
120
src/output/icons.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use ansi_term::Style;
|
||||
use fs::File;
|
||||
use info::filetype::FileExtensions;
|
||||
use output::file_name::FileStyle;
|
||||
|
||||
pub trait FileIcon {
|
||||
fn icon_file(&self, file: &File) -> Option<char>;
|
||||
}
|
||||
|
||||
pub enum Icons {
|
||||
Audio,
|
||||
Image,
|
||||
Video,
|
||||
}
|
||||
|
||||
impl Icons {
|
||||
pub fn value(&self) -> char {
|
||||
match *self {
|
||||
Icons::Audio => '\u{f001}',
|
||||
Icons::Image => '\u{f1c5}',
|
||||
Icons::Video => '\u{f03d}',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn painted_icon(file: &File, style: &FileStyle) -> String {
|
||||
let file_icon = icon(&file).to_string();
|
||||
let painted = style.exts
|
||||
.colour_file(&file)
|
||||
.map_or(file_icon.to_string(), |c| {
|
||||
// Remove underline from icon
|
||||
if c.is_underline {
|
||||
match c.foreground {
|
||||
Some(color) => Style::from(color).paint(file_icon).to_string(),
|
||||
None => Style::default().paint(file_icon).to_string(),
|
||||
}
|
||||
} else {
|
||||
c.paint(file_icon).to_string()
|
||||
}
|
||||
});
|
||||
format!("{} ", painted)
|
||||
}
|
||||
|
||||
fn icon(file: &File) -> char {
|
||||
let extensions = Box::new(FileExtensions);
|
||||
if file.is_directory() { '\u{f115}' }
|
||||
else if let Some(icon) = extensions.icon_file(file) { icon }
|
||||
else {
|
||||
if let Some(ext) = file.ext.as_ref() {
|
||||
match ext.as_str() {
|
||||
"ai" => '\u{e7b4}',
|
||||
"android" => '\u{e70e}',
|
||||
"apple" => '\u{f179}',
|
||||
"avro" => '\u{e60b}',
|
||||
"c" => '\u{e61e}',
|
||||
"clj" => '\u{e768}',
|
||||
"coffee" => '\u{f0f4}',
|
||||
"conf" => '\u{e615}',
|
||||
"cpp" => '\u{e61d}',
|
||||
"css" => '\u{e749}',
|
||||
"d" => '\u{e7af}',
|
||||
"dart" => '\u{e798}',
|
||||
"db" => '\u{f1c0}',
|
||||
"diff" => '\u{f440}',
|
||||
"doc" => '\u{f1c2}',
|
||||
"ebook" => '\u{e28b}',
|
||||
"env" => '\u{f462}',
|
||||
"epub" => '\u{e28a}',
|
||||
"erl" => '\u{e7b1}',
|
||||
"font" => '\u{f031}',
|
||||
"gform" => '\u{f298}',
|
||||
"git" => '\u{f1d3}',
|
||||
"go" => '\u{e626}',
|
||||
"hs" => '\u{e777}',
|
||||
"html" => '\u{f13b}',
|
||||
"iml" => '\u{e7b5}',
|
||||
"java" => '\u{e204}',
|
||||
"js" => '\u{e74e}',
|
||||
"json" => '\u{e60b}',
|
||||
"jsx" => '\u{e7ba}',
|
||||
"less" => '\u{e758}',
|
||||
"log" => '\u{f18d}',
|
||||
"lua" => '\u{e620}',
|
||||
"md" => '\u{f48a}',
|
||||
"mustache" => '\u{e60f}',
|
||||
"npmignore" => '\u{e71e}',
|
||||
"pdf" => '\u{f1c1}',
|
||||
"php" => '\u{e73d}',
|
||||
"pl" => '\u{e769}',
|
||||
"ppt" => '\u{f1c4}',
|
||||
"psd" => '\u{e7b8}',
|
||||
"py" => '\u{e606}',
|
||||
"r" => '\u{f25d}',
|
||||
"rb" => '\u{e21e}',
|
||||
"rdb" => '\u{e76d}',
|
||||
"rs" => '\u{e7a8}',
|
||||
"rss" => '\u{f09e}',
|
||||
"rubydoc" => '\u{e73b}',
|
||||
"sass" => '\u{e603}',
|
||||
"scala" => '\u{e737}',
|
||||
"shell" => '\u{f489}',
|
||||
"sqlite3" => '\u{e7c4}',
|
||||
"styl" => '\u{e600}',
|
||||
"tex" => '\u{e600}',
|
||||
"ts" => '\u{e628}',
|
||||
"twig" => '\u{e61c}',
|
||||
"txt" => '\u{f15c}',
|
||||
"video" => '\u{f03d}',
|
||||
"vim" => '\u{e62b}',
|
||||
"xls" => '\u{f1c3}',
|
||||
"xml" => '\u{e619}',
|
||||
"yml" => '\u{f481}',
|
||||
"zip" => '\u{f410}',
|
||||
_ => '\u{f15b}'
|
||||
}
|
||||
} else {
|
||||
'\u{f15b}'
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +1,46 @@
|
||||
use std::io::{Write, Result as IOResult};
|
||||
|
||||
use ansi_term::ANSIStrings;
|
||||
use ansi_term::{ANSIStrings, ANSIGenericString};
|
||||
|
||||
use fs::File;
|
||||
|
||||
use output::file_name::{FileName, FileStyle};
|
||||
use super::colours::Colours;
|
||||
use style::Colours;
|
||||
use output::icons::painted_icon;
|
||||
use output::cell::TextCell;
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct Options {
|
||||
pub icons: bool
|
||||
}
|
||||
|
||||
/// The lines view literally just displays each file, line-by-line.
|
||||
pub struct Render<'a> {
|
||||
pub files: Vec<File<'a>>,
|
||||
pub colours: &'a Colours,
|
||||
pub style: &'a FileStyle,
|
||||
pub opts: &'a Options,
|
||||
}
|
||||
|
||||
impl<'a> Render<'a> {
|
||||
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
|
||||
for file in &self.files {
|
||||
let name_cell = self.render_file(file).paint();
|
||||
writeln!(w, "{}", ANSIStrings(&name_cell))?;
|
||||
if self.opts.icons {
|
||||
// Create a TextCell for the icon then append the text to it
|
||||
let mut cell = TextCell::default();
|
||||
let icon = painted_icon(&file, self.style);
|
||||
cell.push(ANSIGenericString::from(icon), 2);
|
||||
cell.append(name_cell.promote());
|
||||
writeln!(w, "{}", ANSIStrings(&cell))?;
|
||||
} else {
|
||||
writeln!(w, "{}", ANSIStrings(&name_cell))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_file<'f>(&self, file: &'f File<'a>) -> FileName<'f, 'a> {
|
||||
fn render_file<'f>(&self, file: &'f File<'a>) -> FileName<'f, 'a, Colours> {
|
||||
self.style.for_file(file, self.colours).with_link_paths()
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,39 @@
|
||||
use output::file_name::FileStyle;
|
||||
use style::Colours;
|
||||
|
||||
pub use self::cell::{TextCell, TextCellContents, DisplayWidth};
|
||||
pub use self::colours::Colours;
|
||||
pub use self::escape::escape;
|
||||
|
||||
pub mod details;
|
||||
pub mod file_name;
|
||||
pub mod grid_details;
|
||||
pub mod grid;
|
||||
pub mod icons;
|
||||
pub mod lines;
|
||||
pub mod render;
|
||||
pub mod table;
|
||||
pub mod time;
|
||||
|
||||
mod cell;
|
||||
mod colours;
|
||||
mod escape;
|
||||
mod render;
|
||||
mod tree;
|
||||
|
||||
|
||||
/// The **view** contains all information about how to format output.
|
||||
#[derive(Debug)]
|
||||
pub struct View {
|
||||
pub mode: Mode,
|
||||
pub colours: Colours,
|
||||
pub style: FileStyle,
|
||||
}
|
||||
|
||||
|
||||
/// The **mode** is the “type” of output.
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Mode {
|
||||
Grid(grid::Options),
|
||||
Details(details::Options),
|
||||
GridDetails(grid_details::Options),
|
||||
Lines(lines::Options),
|
||||
}
|
||||
|
@ -1,44 +1,57 @@
|
||||
use ansi_term::Style;
|
||||
|
||||
use output::cell::TextCell;
|
||||
use output::colours::Colours;
|
||||
use fs::fields as f;
|
||||
|
||||
|
||||
impl f::Blocks {
|
||||
pub fn render(&self, colours: &Colours) -> TextCell {
|
||||
pub fn render<C: Colours>(&self, colours: &C) -> TextCell {
|
||||
match *self {
|
||||
f::Blocks::Some(ref blk) => TextCell::paint(colours.blocks, blk.to_string()),
|
||||
f::Blocks::None => TextCell::blank(colours.punctuation),
|
||||
f::Blocks::Some(ref blk) => TextCell::paint(colours.block_count(), blk.to_string()),
|
||||
f::Blocks::None => TextCell::blank(colours.no_blocks()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait Colours {
|
||||
fn block_count(&self) -> Style;
|
||||
fn no_blocks(&self) -> Style;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use output::colours::Colours;
|
||||
use ansi_term::Style;
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
use super::Colours;
|
||||
use output::cell::TextCell;
|
||||
use fs::fields as f;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
struct TestColours;
|
||||
|
||||
impl Colours for TestColours {
|
||||
fn block_count(&self) -> Style { Red.blink() }
|
||||
fn no_blocks(&self) -> Style { Green.italic() }
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn blocklessness() {
|
||||
let mut colours = Colours::default();
|
||||
colours.punctuation = Green.italic();
|
||||
|
||||
let blox = f::Blocks::None;
|
||||
let expected = TextCell::blank(Green.italic());
|
||||
assert_eq!(expected, blox.render(&colours).into());
|
||||
|
||||
assert_eq!(expected, blox.render(&TestColours).into());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn blockfulity() {
|
||||
let mut colours = Colours::default();
|
||||
colours.blocks = Red.blink();
|
||||
|
||||
let blox = f::Blocks::Some(3005);
|
||||
let expected = TextCell::paint_str(Red.blink(), "3005");
|
||||
assert_eq!(expected, blox.render(&colours).into());
|
||||
|
||||
assert_eq!(expected, blox.render(&TestColours).into());
|
||||
}
|
||||
}
|
||||
|
31
src/output/render/filetype.rs
Normal file
31
src/output/render/filetype.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use ansi_term::{ANSIString, Style};
|
||||
|
||||
use fs::fields as f;
|
||||
|
||||
|
||||
impl f::Type {
|
||||
pub fn render<C: Colours>(&self, colours: &C) -> ANSIString<'static> {
|
||||
match *self {
|
||||
f::Type::File => colours.normal().paint("."),
|
||||
f::Type::Directory => colours.directory().paint("d"),
|
||||
f::Type::Pipe => colours.pipe().paint("|"),
|
||||
f::Type::Link => colours.symlink().paint("l"),
|
||||
f::Type::BlockDevice => colours.block_device().paint("b"),
|
||||
f::Type::CharDevice => colours.char_device().paint("c"),
|
||||
f::Type::Socket => colours.socket().paint("s"),
|
||||
f::Type::Special => colours.special().paint("?"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait Colours {
|
||||
fn normal(&self) -> Style;
|
||||
fn directory(&self) -> Style;
|
||||
fn pipe(&self) -> Style;
|
||||
fn symlink(&self) -> Style;
|
||||
fn block_device(&self) -> Style;
|
||||
fn char_device(&self) -> Style;
|
||||
fn socket(&self) -> Style;
|
||||
fn special(&self) -> Style;
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
use ansi_term::ANSIString;
|
||||
use ansi_term::{ANSIString, Style};
|
||||
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use output::colours::Colours;
|
||||
use fs::fields as f;
|
||||
|
||||
|
||||
@ -17,34 +16,58 @@ impl f::Git {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl f::GitStatus {
|
||||
fn render(&self, colours: &Colours) -> ANSIString<'static> {
|
||||
match *self {
|
||||
f::GitStatus::NotModified => colours.punctuation.paint("-"),
|
||||
f::GitStatus::New => colours.git.new.paint("N"),
|
||||
f::GitStatus::Modified => colours.git.modified.paint("M"),
|
||||
f::GitStatus::Deleted => colours.git.deleted.paint("D"),
|
||||
f::GitStatus::Renamed => colours.git.renamed.paint("R"),
|
||||
f::GitStatus::TypeChange => colours.git.typechange.paint("T"),
|
||||
f::GitStatus::NotModified => colours.not_modified().paint("-"),
|
||||
f::GitStatus::New => colours.new().paint("N"),
|
||||
f::GitStatus::Modified => colours.modified().paint("M"),
|
||||
f::GitStatus::Deleted => colours.deleted().paint("D"),
|
||||
f::GitStatus::Renamed => colours.renamed().paint("R"),
|
||||
f::GitStatus::TypeChange => colours.type_change().paint("T"),
|
||||
f::GitStatus::Ignored => colours.ignored().paint("I"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait Colours {
|
||||
fn not_modified(&self) -> Style;
|
||||
fn new(&self) -> Style;
|
||||
fn modified(&self) -> Style;
|
||||
fn deleted(&self) -> Style;
|
||||
fn renamed(&self) -> Style;
|
||||
fn type_change(&self) -> Style;
|
||||
fn ignored(&self) -> Style;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use output::colours::Colours;
|
||||
use super::Colours;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use fs::fields as f;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
use ansi_term::Style;
|
||||
|
||||
|
||||
struct TestColours;
|
||||
|
||||
impl Colours for TestColours {
|
||||
fn not_modified(&self) -> Style { Fixed(90).normal() }
|
||||
fn new(&self) -> Style { Fixed(91).normal() }
|
||||
fn modified(&self) -> Style { Fixed(92).normal() }
|
||||
fn deleted(&self) -> Style { Fixed(93).normal() }
|
||||
fn renamed(&self) -> Style { Fixed(94).normal() }
|
||||
fn type_change(&self) -> Style { Fixed(95).normal() }
|
||||
fn ignored(&self) -> Style { Fixed(96).normal() }
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn git_blank() {
|
||||
let mut colours = Colours::default();
|
||||
colours.punctuation = Fixed(44).normal();
|
||||
|
||||
let stati = f::Git {
|
||||
staged: f::GitStatus::NotModified,
|
||||
unstaged: f::GitStatus::NotModified,
|
||||
@ -53,21 +76,17 @@ pub mod test {
|
||||
let expected = TextCell {
|
||||
width: DisplayWidth::from(2),
|
||||
contents: vec![
|
||||
Fixed(44).paint("-"),
|
||||
Fixed(44).paint("-"),
|
||||
Fixed(90).paint("-"),
|
||||
Fixed(90).paint("-"),
|
||||
].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&colours).into())
|
||||
assert_eq!(expected, stati.render(&TestColours).into())
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn git_new_changed() {
|
||||
let mut colours = Colours::default();
|
||||
colours.git.new = Red.normal();
|
||||
colours.git.modified = Purple.normal();
|
||||
|
||||
let stati = f::Git {
|
||||
staged: f::GitStatus::New,
|
||||
unstaged: f::GitStatus::Modified,
|
||||
@ -76,11 +95,11 @@ pub mod test {
|
||||
let expected = TextCell {
|
||||
width: DisplayWidth::from(2),
|
||||
contents: vec![
|
||||
Red.paint("N"),
|
||||
Purple.paint("M"),
|
||||
Fixed(91).paint("N"),
|
||||
Fixed(92).paint("M"),
|
||||
].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&colours).into())
|
||||
assert_eq!(expected, stati.render(&TestColours).into())
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
use ansi_term::Style;
|
||||
use users::{Users, Groups};
|
||||
|
||||
use fs::fields as f;
|
||||
use output::colours::Colours;
|
||||
use output::cell::TextCell;
|
||||
|
||||
|
||||
impl f::Group {
|
||||
pub fn render<U: Users+Groups>(&self, colours: &Colours, users: &U) -> TextCell {
|
||||
pub fn render<C: Colours, U: Users+Groups>(&self, colours: &C, users: &U) -> TextCell {
|
||||
use users::os::unix::GroupExt;
|
||||
|
||||
let mut style = colours.users.group_not_yours;
|
||||
let mut style = colours.not_yours();
|
||||
|
||||
let group = match users.get_group_by_gid(self.0) {
|
||||
Some(g) => (*g).clone(),
|
||||
@ -19,73 +19,76 @@ impl f::Group {
|
||||
let current_uid = users.get_current_uid();
|
||||
if let Some(current_user) = users.get_user_by_uid(current_uid) {
|
||||
if current_user.primary_group_id() == group.gid()
|
||||
|| group.members().contains(¤t_user.name().to_owned()) {
|
||||
style = colours.users.group_yours;
|
||||
|| group.members().iter().any(|u| u == current_user.name()) {
|
||||
style = colours.yours();
|
||||
}
|
||||
}
|
||||
|
||||
TextCell::paint(style, group.name().to_owned())
|
||||
TextCell::paint(style, group.name().to_string_lossy().into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait Colours {
|
||||
fn yours(&self) -> Style;
|
||||
fn not_yours(&self) -> Style;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(unused_results)]
|
||||
pub mod test {
|
||||
use super::Colours;
|
||||
use fs::fields as f;
|
||||
use output::cell::TextCell;
|
||||
use output::colours::Colours;
|
||||
|
||||
use users::{User, Group};
|
||||
use users::mock::MockUsers;
|
||||
use users::os::unix::GroupExt;
|
||||
use ansi_term::Colour::*;
|
||||
use ansi_term::Style;
|
||||
|
||||
|
||||
struct TestColours;
|
||||
|
||||
impl Colours for TestColours {
|
||||
fn yours(&self) -> Style { Fixed(80).normal() }
|
||||
fn not_yours(&self) -> Style { Fixed(81).normal() }
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn named() {
|
||||
let mut colours = Colours::default();
|
||||
colours.users.group_not_yours = Fixed(101).normal();
|
||||
|
||||
let mut users = MockUsers::with_current_uid(1000);
|
||||
users.add_group(Group::new(100, "folk"));
|
||||
|
||||
let group = f::Group(100);
|
||||
let expected = TextCell::paint_str(Fixed(101).normal(), "folk");
|
||||
assert_eq!(expected, group.render(&colours, &users))
|
||||
let expected = TextCell::paint_str(Fixed(81).normal(), "folk");
|
||||
assert_eq!(expected, group.render(&TestColours, &users))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnamed() {
|
||||
let mut colours = Colours::default();
|
||||
colours.users.group_not_yours = Fixed(87).normal();
|
||||
|
||||
let users = MockUsers::with_current_uid(1000);
|
||||
|
||||
let group = f::Group(100);
|
||||
let expected = TextCell::paint_str(Fixed(87).normal(), "100");
|
||||
assert_eq!(expected, group.render(&colours, &users));
|
||||
let expected = TextCell::paint_str(Fixed(81).normal(), "100");
|
||||
assert_eq!(expected, group.render(&TestColours, &users));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn primary() {
|
||||
let mut colours = Colours::default();
|
||||
colours.users.group_yours = Fixed(64).normal();
|
||||
|
||||
let mut users = MockUsers::with_current_uid(2);
|
||||
users.add_user(User::new(2, "eve", 100));
|
||||
users.add_group(Group::new(100, "folk"));
|
||||
|
||||
let group = f::Group(100);
|
||||
let expected = TextCell::paint_str(Fixed(64).normal(), "folk");
|
||||
assert_eq!(expected, group.render(&colours, &users))
|
||||
let expected = TextCell::paint_str(Fixed(80).normal(), "folk");
|
||||
assert_eq!(expected, group.render(&TestColours, &users))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secondary() {
|
||||
let mut colours = Colours::default();
|
||||
colours.users.group_yours = Fixed(31).normal();
|
||||
|
||||
let mut users = MockUsers::with_current_uid(2);
|
||||
users.add_user(User::new(2, "eve", 666));
|
||||
|
||||
@ -93,17 +96,14 @@ pub mod test {
|
||||
users.add_group(test_group);
|
||||
|
||||
let group = f::Group(100);
|
||||
let expected = TextCell::paint_str(Fixed(31).normal(), "folk");
|
||||
assert_eq!(expected, group.render(&colours, &users))
|
||||
let expected = TextCell::paint_str(Fixed(80).normal(), "folk");
|
||||
assert_eq!(expected, group.render(&TestColours, &users))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow() {
|
||||
let mut colours = Colours::default();
|
||||
colours.users.group_not_yours = Blue.underline();
|
||||
|
||||
let group = f::Group(2_147_483_648);
|
||||
let expected = TextCell::paint_str(Blue.underline(), "2147483648");
|
||||
assert_eq!(expected, group.render(&colours, &MockUsers::with_current_uid(0)));
|
||||
let expected = TextCell::paint_str(Fixed(81).normal(), "2147483648");
|
||||
assert_eq!(expected, group.render(&TestColours, &MockUsers::with_current_uid(0)));
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
use ansi_term::Style;
|
||||
|
||||
use output::cell::TextCell;
|
||||
use output::colours::Colours;
|
||||
use fs::fields as f;
|
||||
|
||||
|
||||
impl f::Inode {
|
||||
pub fn render(&self, colours: &Colours) -> TextCell {
|
||||
TextCell::paint(colours.inode, self.0.to_string())
|
||||
pub fn render(&self, style: Style) -> TextCell {
|
||||
TextCell::paint(style, self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use output::colours::Colours;
|
||||
use output::cell::TextCell;
|
||||
use fs::fields as f;
|
||||
|
||||
@ -21,11 +21,8 @@ pub mod test {
|
||||
|
||||
#[test]
|
||||
fn blocklessness() {
|
||||
let mut colours = Colours::default();
|
||||
colours.inode = Cyan.underline();
|
||||
|
||||
let io = f::Inode(1414213);
|
||||
let expected = TextCell::paint_str(Cyan.underline(), "1414213");
|
||||
assert_eq!(expected, io.render(&colours).into());
|
||||
assert_eq!(expected, io.render(Cyan.underline()).into());
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +1,47 @@
|
||||
use output::cell::TextCell;
|
||||
use output::colours::Colours;
|
||||
use fs::fields as f;
|
||||
use ansi_term::Style;
|
||||
use locale::Numeric as NumericLocale;
|
||||
|
||||
use locale;
|
||||
use output::cell::TextCell;
|
||||
use fs::fields as f;
|
||||
|
||||
|
||||
impl f::Links {
|
||||
pub fn render(&self, colours: &Colours, numeric: &locale::Numeric) -> TextCell {
|
||||
let style = if self.multiple { colours.links.multi_link_file }
|
||||
else { colours.links.normal };
|
||||
pub fn render<C: Colours>(&self, colours: &C, numeric: &NumericLocale) -> TextCell {
|
||||
let style = if self.multiple { colours.multi_link_file() }
|
||||
else { colours.normal() };
|
||||
|
||||
TextCell::paint(style, numeric.format_int(self.count))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait Colours {
|
||||
fn normal(&self) -> Style;
|
||||
fn multi_link_file(&self) -> Style;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use output::colours::Colours;
|
||||
use super::Colours;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use fs::fields as f;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
use ansi_term::Style;
|
||||
use locale;
|
||||
|
||||
|
||||
struct TestColours;
|
||||
|
||||
impl Colours for TestColours {
|
||||
fn normal(&self) -> Style { Blue.normal() }
|
||||
fn multi_link_file(&self) -> Style { Blue.on(Red) }
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn regular_file() {
|
||||
let mut colours = Colours::default();
|
||||
colours.links.normal = Blue.normal();
|
||||
|
||||
let stati = f::Links {
|
||||
count: 1,
|
||||
multiple: false,
|
||||
@ -40,14 +52,11 @@ pub mod test {
|
||||
contents: vec![ Blue.paint("1") ].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&colours, &locale::Numeric::english()).into());
|
||||
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()).into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regular_directory() {
|
||||
let mut colours = Colours::default();
|
||||
colours.links.normal = Blue.normal();
|
||||
|
||||
let stati = f::Links {
|
||||
count: 3005,
|
||||
multiple: false,
|
||||
@ -58,14 +67,11 @@ pub mod test {
|
||||
contents: vec![ Blue.paint("3,005") ].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&colours, &locale::Numeric::english()).into());
|
||||
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()).into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn popular_file() {
|
||||
let mut colours = Colours::default();
|
||||
colours.links.multi_link_file = Blue.on(Red);
|
||||
|
||||
let stati = f::Links {
|
||||
count: 3005,
|
||||
multiple: true,
|
||||
@ -76,6 +82,6 @@ pub mod test {
|
||||
contents: vec![ Blue.on(Red).paint("3,005") ].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&colours, &locale::Numeric::english()).into());
|
||||
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()).into());
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,30 @@
|
||||
mod blocks;
|
||||
pub use self::blocks::Colours as BlocksColours;
|
||||
|
||||
mod filetype;
|
||||
pub use self::filetype::Colours as FiletypeColours;
|
||||
|
||||
mod git;
|
||||
pub use self::git::Colours as GitColours;
|
||||
|
||||
mod groups;
|
||||
pub use self::groups::Colours as GroupColours;
|
||||
|
||||
mod inode;
|
||||
// inode uses just one colour
|
||||
|
||||
mod links;
|
||||
pub use self::links::Colours as LinksColours;
|
||||
|
||||
mod permissions;
|
||||
pub use self::permissions::Colours as PermissionsColours;
|
||||
|
||||
mod size;
|
||||
pub use self::size::Colours as SizeColours;
|
||||
|
||||
mod times;
|
||||
pub use self::times::Render as TimeRender;
|
||||
// times does too
|
||||
|
||||
mod users;
|
||||
pub use self::users::Colours as UserColours;
|
||||
|
@ -1,16 +1,17 @@
|
||||
use fs::fields as f;
|
||||
use output::colours::Colours;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use ansi_term::{ANSIString, Style};
|
||||
|
||||
use fs::fields as f;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use output::render::FiletypeColours;
|
||||
|
||||
|
||||
impl f::PermissionsPlus {
|
||||
pub fn render(&self, colours: &Colours) -> TextCell {
|
||||
pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
|
||||
let mut chars = vec![ self.file_type.render(colours) ];
|
||||
chars.extend(self.permissions.render(colours, self.file_type.is_regular_file()));
|
||||
|
||||
if self.xattrs {
|
||||
chars.push(colours.perms.attribute.paint("@"));
|
||||
chars.push(colours.attribute().paint("@"));
|
||||
}
|
||||
|
||||
// As these are all ASCII characters, we can guarantee that they’re
|
||||
@ -23,87 +24,115 @@ impl f::PermissionsPlus {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl f::Permissions {
|
||||
pub fn render(&self, colours: &Colours, is_regular_file: bool) -> Vec<ANSIString<'static>> {
|
||||
pub fn render<C: Colours>(&self, colours: &C, is_regular_file: bool) -> Vec<ANSIString<'static>> {
|
||||
|
||||
let bit = |bit, chr: &'static str, style: Style| {
|
||||
if bit { style.paint(chr) } else { colours.punctuation.paint("-") }
|
||||
if bit { style.paint(chr) } else { colours.dash().paint("-") }
|
||||
};
|
||||
|
||||
vec![
|
||||
bit(self.user_read, "r", colours.perms.user_read),
|
||||
bit(self.user_write, "w", colours.perms.user_write),
|
||||
bit(self.user_read, "r", colours.user_read()),
|
||||
bit(self.user_write, "w", colours.user_write()),
|
||||
self.user_execute_bit(colours, is_regular_file),
|
||||
bit(self.group_read, "r", colours.perms.group_read),
|
||||
bit(self.group_write, "w", colours.perms.group_write),
|
||||
bit(self.group_read, "r", colours.group_read()),
|
||||
bit(self.group_write, "w", colours.group_write()),
|
||||
self.group_execute_bit(colours),
|
||||
bit(self.other_read, "r", colours.perms.other_read),
|
||||
bit(self.other_write, "w", colours.perms.other_write),
|
||||
bit(self.other_read, "r", colours.other_read()),
|
||||
bit(self.other_write, "w", colours.other_write()),
|
||||
self.other_execute_bit(colours)
|
||||
]
|
||||
}
|
||||
|
||||
fn user_execute_bit(&self, colours: &Colours, is_regular_file: bool) -> ANSIString<'static> {
|
||||
fn user_execute_bit<C: Colours>(&self, colours: &C, is_regular_file: bool) -> ANSIString<'static> {
|
||||
match (self.user_execute, self.setuid, is_regular_file) {
|
||||
(false, false, _) => colours.punctuation.paint("-"),
|
||||
(true, false, false) => colours.perms.user_execute_other.paint("x"),
|
||||
(true, false, true) => colours.perms.user_execute_file.paint("x"),
|
||||
(false, true, _) => colours.perms.special_other.paint("S"),
|
||||
(true, true, false) => colours.perms.special_other.paint("s"),
|
||||
(true, true, true) => colours.perms.special_user_file.paint("s"),
|
||||
(false, false, _) => colours.dash().paint("-"),
|
||||
(true, false, false) => colours.user_execute_other().paint("x"),
|
||||
(true, false, true) => colours.user_execute_file().paint("x"),
|
||||
(false, true, _) => colours.special_other().paint("S"),
|
||||
(true, true, false) => colours.special_other().paint("s"),
|
||||
(true, true, true) => colours.special_user_file().paint("s"),
|
||||
}
|
||||
}
|
||||
|
||||
fn group_execute_bit(&self, colours: &Colours) -> ANSIString<'static> {
|
||||
fn group_execute_bit<C: Colours>(&self, colours: &C) -> ANSIString<'static> {
|
||||
match (self.group_execute, self.setgid) {
|
||||
(false, false) => colours.punctuation.paint("-"),
|
||||
(true, false) => colours.perms.group_execute.paint("x"),
|
||||
(false, true) => colours.perms.special_other.paint("S"),
|
||||
(true, true) => colours.perms.special_other.paint("s"),
|
||||
(false, false) => colours.dash().paint("-"),
|
||||
(true, false) => colours.group_execute().paint("x"),
|
||||
(false, true) => colours.special_other().paint("S"),
|
||||
(true, true) => colours.special_other().paint("s"),
|
||||
}
|
||||
}
|
||||
|
||||
fn other_execute_bit(&self, colours: &Colours) -> ANSIString<'static> {
|
||||
fn other_execute_bit<C: Colours>(&self, colours: &C) -> ANSIString<'static> {
|
||||
match (self.other_execute, self.sticky) {
|
||||
(false, false) => colours.punctuation.paint("-"),
|
||||
(true, false) => colours.perms.other_execute.paint("x"),
|
||||
(false, true) => colours.perms.special_other.paint("T"),
|
||||
(true, true) => colours.perms.special_other.paint("t"),
|
||||
(false, false) => colours.dash().paint("-"),
|
||||
(true, false) => colours.other_execute().paint("x"),
|
||||
(false, true) => colours.special_other().paint("T"),
|
||||
(true, true) => colours.special_other().paint("t"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl f::Type {
|
||||
pub fn render(&self, colours: &Colours) -> ANSIString<'static> {
|
||||
match *self {
|
||||
f::Type::File => colours.filetypes.normal.paint("."),
|
||||
f::Type::Directory => colours.filetypes.directory.paint("d"),
|
||||
f::Type::Pipe => colours.filetypes.pipe.paint("|"),
|
||||
f::Type::Link => colours.filetypes.symlink.paint("l"),
|
||||
f::Type::CharDevice => colours.filetypes.device.paint("c"),
|
||||
f::Type::BlockDevice => colours.filetypes.device.paint("b"),
|
||||
f::Type::Socket => colours.filetypes.socket.paint("s"),
|
||||
f::Type::Special => colours.filetypes.special.paint("?"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Colours {
|
||||
fn dash(&self) -> Style;
|
||||
|
||||
fn user_read(&self) -> Style;
|
||||
fn user_write(&self) -> Style;
|
||||
fn user_execute_file(&self) -> Style;
|
||||
fn user_execute_other(&self) -> Style;
|
||||
|
||||
fn group_read(&self) -> Style;
|
||||
fn group_write(&self) -> Style;
|
||||
fn group_execute(&self) -> Style;
|
||||
|
||||
fn other_read(&self) -> Style;
|
||||
fn other_write(&self) -> Style;
|
||||
fn other_execute(&self) -> Style;
|
||||
|
||||
fn special_user_file(&self) -> Style;
|
||||
fn special_other(&self) -> Style;
|
||||
|
||||
fn attribute(&self) -> Style;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(unused_results)]
|
||||
pub mod test {
|
||||
use output::colours::Colours;
|
||||
use super::Colours;
|
||||
use output::cell::TextCellContents;
|
||||
use fs::fields as f;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
use ansi_term::Style;
|
||||
|
||||
|
||||
struct TestColours;
|
||||
|
||||
impl Colours for TestColours {
|
||||
fn dash(&self) -> Style { Fixed(11).normal() }
|
||||
fn user_read(&self) -> Style { Fixed(101).normal() }
|
||||
fn user_write(&self) -> Style { Fixed(102).normal() }
|
||||
fn user_execute_file(&self) -> Style { Fixed(103).normal() }
|
||||
fn user_execute_other(&self) -> Style { Fixed(113).normal() }
|
||||
fn group_read(&self) -> Style { Fixed(104).normal() }
|
||||
fn group_write(&self) -> Style { Fixed(105).normal() }
|
||||
fn group_execute(&self) -> Style { Fixed(106).normal() }
|
||||
fn other_read(&self) -> Style { Fixed(107).normal() }
|
||||
fn other_write(&self) -> Style { Fixed(108).normal() }
|
||||
fn other_execute(&self) -> Style { Fixed(109).normal() }
|
||||
fn special_user_file(&self) -> Style { Fixed(110).normal() }
|
||||
fn special_other(&self) -> Style { Fixed(111).normal() }
|
||||
fn attribute(&self) -> Style { Fixed(112).normal() }
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn negate() {
|
||||
let mut colours = Colours::default();
|
||||
colours.punctuation = Fixed(11).normal();
|
||||
|
||||
let bits = f::Permissions {
|
||||
user_read: false, user_write: false, user_execute: false, setuid: false,
|
||||
group_read: false, group_write: false, group_execute: false, setgid: false,
|
||||
@ -116,25 +145,12 @@ pub mod test {
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(11).paint("-"),
|
||||
]);
|
||||
|
||||
assert_eq!(expected, bits.render(&colours, false).into())
|
||||
assert_eq!(expected, bits.render(&TestColours, false).into())
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn affirm() {
|
||||
let mut colours = Colours::default();
|
||||
colours.perms.user_read = Fixed(101).normal();
|
||||
colours.perms.user_write = Fixed(102).normal();
|
||||
colours.perms.user_execute_file = Fixed(103).normal();
|
||||
|
||||
colours.perms.group_read = Fixed(104).normal();
|
||||
colours.perms.group_write = Fixed(105).normal();
|
||||
colours.perms.group_execute = Fixed(106).normal();
|
||||
|
||||
colours.perms.other_read = Fixed(107).normal();
|
||||
colours.perms.other_write = Fixed(108).normal();
|
||||
colours.perms.other_execute = Fixed(109).normal();
|
||||
|
||||
let bits = f::Permissions {
|
||||
user_read: true, user_write: true, user_execute: true, setuid: false,
|
||||
group_read: true, group_write: true, group_execute: true, setgid: false,
|
||||
@ -147,17 +163,12 @@ pub mod test {
|
||||
Fixed(107).paint("r"), Fixed(108).paint("w"), Fixed(109).paint("x"),
|
||||
]);
|
||||
|
||||
assert_eq!(expected, bits.render(&colours, true).into())
|
||||
assert_eq!(expected, bits.render(&TestColours, true).into())
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn specials() {
|
||||
let mut colours = Colours::default();
|
||||
colours.punctuation = Fixed(11).normal();
|
||||
colours.perms.special_user_file = Fixed(77).normal();
|
||||
colours.perms.special_other = Fixed(88).normal();
|
||||
|
||||
let bits = f::Permissions {
|
||||
user_read: false, user_write: false, user_execute: true, setuid: true,
|
||||
group_read: false, group_write: false, group_execute: true, setgid: true,
|
||||
@ -165,21 +176,17 @@ pub mod test {
|
||||
};
|
||||
|
||||
let expected = TextCellContents::from(vec![
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(77).paint("s"),
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(88).paint("s"),
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(88).paint("t"),
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(110).paint("s"),
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("s"),
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("t"),
|
||||
]);
|
||||
|
||||
assert_eq!(expected, bits.render(&colours, true).into())
|
||||
assert_eq!(expected, bits.render(&TestColours, true).into())
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn extra_specials() {
|
||||
let mut colours = Colours::default();
|
||||
colours.punctuation = Fixed(11).normal();
|
||||
colours.perms.special_other = Fixed(88).normal();
|
||||
|
||||
let bits = f::Permissions {
|
||||
user_read: false, user_write: false, user_execute: false, setuid: true,
|
||||
group_read: false, group_write: false, group_execute: false, setgid: true,
|
||||
@ -187,11 +194,11 @@ pub mod test {
|
||||
};
|
||||
|
||||
let expected = TextCellContents::from(vec![
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(88).paint("S"),
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(88).paint("S"),
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(88).paint("T"),
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("S"),
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("S"),
|
||||
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("T"),
|
||||
]);
|
||||
|
||||
assert_eq!(expected, bits.render(&colours, true).into())
|
||||
assert_eq!(expected, bits.render(&TestColours, true).into())
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,33 @@
|
||||
use ansi_term::Style;
|
||||
use locale::Numeric as NumericLocale;
|
||||
|
||||
use fs::fields as f;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use output::colours::Colours;
|
||||
use output::table::SizeFormat;
|
||||
use locale;
|
||||
|
||||
|
||||
|
||||
impl f::Size {
|
||||
pub fn render(&self, colours: &Colours, size_format: SizeFormat, numerics: &locale::Numeric) -> TextCell {
|
||||
use number_prefix::{binary_prefix, decimal_prefix};
|
||||
use number_prefix::{Prefixed, Standalone, PrefixNames};
|
||||
pub fn render<C: Colours>(&self, colours: &C, size_format: SizeFormat, numerics: &NumericLocale) -> TextCell {
|
||||
use number_prefix::{Prefixed, Standalone, NumberPrefix, PrefixNames};
|
||||
|
||||
let size = match *self {
|
||||
f::Size::Some(s) => s,
|
||||
f::Size::None => return TextCell::blank(colours.punctuation),
|
||||
f::Size::None => return TextCell::blank(colours.no_size()),
|
||||
f::Size::DeviceIDs(ref ids) => return ids.render(colours),
|
||||
};
|
||||
|
||||
let result = match size_format {
|
||||
SizeFormat::DecimalBytes => decimal_prefix(size as f64),
|
||||
SizeFormat::BinaryBytes => binary_prefix(size as f64),
|
||||
SizeFormat::DecimalBytes => NumberPrefix::decimal(size as f64),
|
||||
SizeFormat::BinaryBytes => NumberPrefix::binary(size as f64),
|
||||
SizeFormat::JustBytes => {
|
||||
let string = numerics.format_int(size);
|
||||
return TextCell::paint(colours.file_size(size), string);
|
||||
return TextCell::paint(colours.size(size), string);
|
||||
},
|
||||
};
|
||||
|
||||
let (prefix, n) = match result {
|
||||
Standalone(b) => return TextCell::paint(colours.file_size(b as u64), b.to_string()),
|
||||
Standalone(b) => return TextCell::paint(colours.size(b as u64), b.to_string()),
|
||||
Prefixed(p, n) => (p, n)
|
||||
};
|
||||
|
||||
@ -39,116 +40,123 @@ impl f::Size {
|
||||
let width = DisplayWidth::from(number.len() + symbol.len());
|
||||
|
||||
TextCell {
|
||||
width: width,
|
||||
width,
|
||||
contents: vec![
|
||||
colours.file_size(size).paint(number),
|
||||
colours.size.unit.paint(symbol),
|
||||
colours.size(size).paint(number),
|
||||
colours.unit().paint(symbol),
|
||||
].into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl f::DeviceIDs {
|
||||
fn render(&self, colours: &Colours) -> TextCell {
|
||||
fn render<C: Colours>(&self, colours: &C) -> TextCell {
|
||||
let major = self.major.to_string();
|
||||
let minor = self.minor.to_string();
|
||||
|
||||
TextCell {
|
||||
width: DisplayWidth::from(major.len() + 1 + minor.len()),
|
||||
contents: vec![
|
||||
colours.size.major.paint(major),
|
||||
colours.punctuation.paint(","),
|
||||
colours.size.minor.paint(minor),
|
||||
colours.major().paint(major),
|
||||
colours.comma().paint(","),
|
||||
colours.minor().paint(minor),
|
||||
].into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait Colours {
|
||||
fn size(&self, size: u64) -> Style;
|
||||
fn unit(&self) -> Style;
|
||||
fn no_size(&self) -> Style;
|
||||
|
||||
fn major(&self) -> Style;
|
||||
fn comma(&self) -> Style;
|
||||
fn minor(&self) -> Style;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use output::colours::Colours;
|
||||
use super::Colours;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use output::table::SizeFormat;
|
||||
use fs::fields as f;
|
||||
|
||||
use locale;
|
||||
use locale::Numeric as NumericLocale;
|
||||
use ansi_term::Colour::*;
|
||||
use ansi_term::Style;
|
||||
|
||||
|
||||
struct TestColours;
|
||||
|
||||
impl Colours for TestColours {
|
||||
fn size(&self, _size: u64) -> Style { Fixed(66).normal() }
|
||||
fn unit(&self) -> Style { Fixed(77).bold() }
|
||||
fn no_size(&self) -> Style { Black.italic() }
|
||||
|
||||
fn major(&self) -> Style { Blue.on(Red) }
|
||||
fn comma(&self) -> Style { Green.italic() }
|
||||
fn minor(&self) -> Style { Cyan.on(Yellow) }
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn directory() {
|
||||
let mut colours = Colours::default();
|
||||
colours.punctuation = Green.italic();
|
||||
|
||||
let directory = f::Size::None;
|
||||
let expected = TextCell::blank(Green.italic());
|
||||
assert_eq!(expected, directory.render(&colours, SizeFormat::JustBytes, &locale::Numeric::english()))
|
||||
let expected = TextCell::blank(Black.italic());
|
||||
assert_eq!(expected, directory.render(&TestColours, SizeFormat::JustBytes, &NumericLocale::english()))
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn file_decimal() {
|
||||
let mut colours = Colours::default();
|
||||
colours.size.numbers = Blue.on(Red);
|
||||
colours.size.unit = Yellow.bold();
|
||||
|
||||
let directory = f::Size::Some(2_100_000);
|
||||
let expected = TextCell {
|
||||
width: DisplayWidth::from(4),
|
||||
contents: vec![
|
||||
Blue.on(Red).paint("2.1"),
|
||||
Yellow.bold().paint("M"),
|
||||
Fixed(66).paint("2.1"),
|
||||
Fixed(77).bold().paint("M"),
|
||||
].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, directory.render(&colours, SizeFormat::DecimalBytes, &locale::Numeric::english()))
|
||||
assert_eq!(expected, directory.render(&TestColours, SizeFormat::DecimalBytes, &NumericLocale::english()))
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn file_binary() {
|
||||
let mut colours = Colours::default();
|
||||
colours.size.numbers = Blue.on(Red);
|
||||
colours.size.unit = Yellow.bold();
|
||||
|
||||
let directory = f::Size::Some(1_048_576);
|
||||
let expected = TextCell {
|
||||
width: DisplayWidth::from(5),
|
||||
contents: vec![
|
||||
Blue.on(Red).paint("1.0"),
|
||||
Yellow.bold().paint("Mi"),
|
||||
Fixed(66).paint("1.0"),
|
||||
Fixed(77).bold().paint("Mi"),
|
||||
].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, directory.render(&colours, SizeFormat::BinaryBytes, &locale::Numeric::english()))
|
||||
assert_eq!(expected, directory.render(&TestColours, SizeFormat::BinaryBytes, &NumericLocale::english()))
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn file_bytes() {
|
||||
let mut colours = Colours::default();
|
||||
colours.size.numbers = Blue.on(Red);
|
||||
|
||||
let directory = f::Size::Some(1048576);
|
||||
let expected = TextCell {
|
||||
width: DisplayWidth::from(9),
|
||||
contents: vec![
|
||||
Blue.on(Red).paint("1,048,576"),
|
||||
Fixed(66).paint("1,048,576"),
|
||||
].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, directory.render(&colours, SizeFormat::JustBytes, &locale::Numeric::english()))
|
||||
assert_eq!(expected, directory.render(&TestColours, SizeFormat::JustBytes, &NumericLocale::english()))
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn device_ids() {
|
||||
let mut colours = Colours::default();
|
||||
colours.size.major = Blue.on(Red);
|
||||
colours.punctuation = Green.italic();
|
||||
colours.size.minor = Cyan.on(Yellow);
|
||||
|
||||
let directory = f::Size::DeviceIDs(f::DeviceIDs { major: 10, minor: 80 });
|
||||
let expected = TextCell {
|
||||
width: DisplayWidth::from(5),
|
||||
@ -159,6 +167,6 @@ pub mod test {
|
||||
].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, directory.render(&colours, SizeFormat::JustBytes, &locale::Numeric::english()))
|
||||
assert_eq!(expected, directory.render(&TestColours, SizeFormat::JustBytes, &NumericLocale::english()))
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,28 @@
|
||||
use datetime::TimeZone;
|
||||
use ansi_term::Style;
|
||||
|
||||
use fs::fields as f;
|
||||
use output::cell::TextCell;
|
||||
use output::colours::Colours;
|
||||
use output::time::TimeFormat;
|
||||
|
||||
|
||||
impl f::Time {
|
||||
pub fn render(self, colours: &Colours,
|
||||
tz: &Option<TimeZone>,
|
||||
style: &TimeFormat) -> TextCell {
|
||||
pub trait Render {
|
||||
fn render(self, style: Style,
|
||||
tz: &Option<TimeZone>,
|
||||
format: &TimeFormat) -> TextCell;
|
||||
}
|
||||
|
||||
impl Render for std::time::Duration {
|
||||
fn render(self, style: Style,
|
||||
tz: &Option<TimeZone>,
|
||||
format: &TimeFormat) -> TextCell {
|
||||
|
||||
if let Some(ref tz) = *tz {
|
||||
let datestamp = style.format_zoned(self, tz);
|
||||
TextCell::paint(colours.date, datestamp)
|
||||
let datestamp = format.format_zoned(self, tz);
|
||||
TextCell::paint(style, datestamp)
|
||||
}
|
||||
else {
|
||||
let datestamp = style.format_local(self);
|
||||
TextCell::paint(colours.date, datestamp)
|
||||
let datestamp = format.format_local(self);
|
||||
TextCell::paint(style, datestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,89 +1,92 @@
|
||||
use ansi_term::Style;
|
||||
use users::Users;
|
||||
|
||||
use fs::fields as f;
|
||||
use output::colours::Colours;
|
||||
use output::cell::TextCell;
|
||||
|
||||
|
||||
|
||||
impl f::User {
|
||||
pub fn render(&self, colours: &Colours, users: &Users) -> TextCell {
|
||||
pub fn render<C: Colours, U: Users>(&self, colours: &C, users: &U) -> TextCell {
|
||||
let user_name = match users.get_user_by_uid(self.0) {
|
||||
Some(user) => user.name().to_owned(),
|
||||
Some(user) => user.name().to_string_lossy().into(),
|
||||
None => self.0.to_string(),
|
||||
};
|
||||
|
||||
let style = if users.get_current_uid() == self.0 { colours.users.user_you }
|
||||
else { colours.users.user_someone_else };
|
||||
let style = if users.get_current_uid() == self.0 { colours.you() }
|
||||
else { colours.someone_else() };
|
||||
TextCell::paint(style, user_name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait Colours {
|
||||
fn you(&self) -> Style;
|
||||
fn someone_else(&self) -> Style;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(unused_results)]
|
||||
pub mod test {
|
||||
use super::Colours;
|
||||
use fs::fields as f;
|
||||
use output::cell::TextCell;
|
||||
use output::colours::Colours;
|
||||
|
||||
use users::User;
|
||||
use users::mock::MockUsers;
|
||||
use ansi_term::Colour::*;
|
||||
use ansi_term::Style;
|
||||
|
||||
|
||||
struct TestColours;
|
||||
|
||||
impl Colours for TestColours {
|
||||
fn you(&self) -> Style { Red.bold() }
|
||||
fn someone_else(&self) -> Style { Blue.underline() }
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn named() {
|
||||
let mut colours = Colours::default();
|
||||
colours.users.user_you = Red.bold();
|
||||
|
||||
let mut users = MockUsers::with_current_uid(1000);
|
||||
users.add_user(User::new(1000, "enoch", 100));
|
||||
|
||||
let user = f::User(1000);
|
||||
let expected = TextCell::paint_str(Red.bold(), "enoch");
|
||||
assert_eq!(expected, user.render(&colours, &users))
|
||||
assert_eq!(expected, user.render(&TestColours, &users))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnamed() {
|
||||
let mut colours = Colours::default();
|
||||
colours.users.user_you = Cyan.bold();
|
||||
|
||||
let users = MockUsers::with_current_uid(1000);
|
||||
|
||||
let user = f::User(1000);
|
||||
let expected = TextCell::paint_str(Cyan.bold(), "1000");
|
||||
assert_eq!(expected, user.render(&colours, &users));
|
||||
let expected = TextCell::paint_str(Red.bold(), "1000");
|
||||
assert_eq!(expected, user.render(&TestColours, &users));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_named() {
|
||||
let mut colours = Colours::default();
|
||||
colours.users.user_someone_else = Green.bold();
|
||||
|
||||
let mut users = MockUsers::with_current_uid(0);
|
||||
users.add_user(User::new(1000, "enoch", 100));
|
||||
|
||||
let user = f::User(1000);
|
||||
let expected = TextCell::paint_str(Green.bold(), "enoch");
|
||||
assert_eq!(expected, user.render(&colours, &users));
|
||||
let expected = TextCell::paint_str(Blue.underline(), "enoch");
|
||||
assert_eq!(expected, user.render(&TestColours, &users));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_unnamed() {
|
||||
let mut colours = Colours::default();
|
||||
colours.users.user_someone_else = Red.normal();
|
||||
|
||||
let user = f::User(1000);
|
||||
let expected = TextCell::paint_str(Red.normal(), "1000");
|
||||
assert_eq!(expected, user.render(&colours, &MockUsers::with_current_uid(0)));
|
||||
let expected = TextCell::paint_str(Blue.underline(), "1000");
|
||||
assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow() {
|
||||
let mut colours = Colours::default();
|
||||
colours.users.user_someone_else = Blue.underline();
|
||||
|
||||
let user = f::User(2_147_483_648);
|
||||
let expected = TextCell::paint_str(Blue.underline(), "2147483648");
|
||||
assert_eq!(expected, user.render(&colours, &MockUsers::with_current_uid(0)));
|
||||
assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0)));
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,12 @@ use locale;
|
||||
|
||||
use users::UsersCache;
|
||||
|
||||
use style::Colours;
|
||||
use output::cell::TextCell;
|
||||
use output::colours::Colours;
|
||||
use output::render::TimeRender;
|
||||
use output::time::TimeFormat;
|
||||
|
||||
use fs::{File, Dir, fields as f};
|
||||
|
||||
use fs::{File, fields as f};
|
||||
use fs::feature::git::GitCache;
|
||||
|
||||
|
||||
/// Options for displaying a table.
|
||||
@ -23,29 +23,35 @@ pub struct Options {
|
||||
pub env: Environment,
|
||||
pub size_format: SizeFormat,
|
||||
pub time_format: TimeFormat,
|
||||
pub extra_columns: Columns,
|
||||
}
|
||||
|
||||
// I had to make other types derive Debug,
|
||||
// and Mutex<UsersCache> is not that!
|
||||
impl fmt::Debug for Options {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
write!(f, "Table({:#?})", self.extra_columns)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extra columns to display in the table.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Columns {
|
||||
|
||||
/// At least one of these timestamps will be shown.
|
||||
pub time_types: TimeTypes,
|
||||
|
||||
// The rest are just on/off
|
||||
pub inode: bool,
|
||||
pub links: bool,
|
||||
pub blocks: bool,
|
||||
pub group: bool,
|
||||
pub git: bool
|
||||
pub git: bool,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Options {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
// I had to make other types derive Debug,
|
||||
// and Mutex<UsersCache> is not that!
|
||||
writeln!(f, "<table options>")
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn should_scan_for_git(&self) -> bool {
|
||||
self.git
|
||||
}
|
||||
|
||||
pub fn for_dir(&self, dir: Option<&Dir>) -> Vec<Column> {
|
||||
let mut columns = vec![];
|
||||
impl Columns {
|
||||
pub fn collect(&self, actually_enable_git: bool) -> Vec<Column> {
|
||||
let mut columns = Vec::with_capacity(4);
|
||||
|
||||
if self.inode {
|
||||
columns.push(Column::Inode);
|
||||
@ -57,7 +63,7 @@ impl Options {
|
||||
columns.push(Column::HardLinks);
|
||||
}
|
||||
|
||||
columns.push(Column::FileSize(self.size_format));
|
||||
columns.push(Column::FileSize);
|
||||
|
||||
if self.blocks {
|
||||
columns.push(Column::Blocks);
|
||||
@ -73,6 +79,10 @@ impl Options {
|
||||
columns.push(Column::Timestamp(TimeType::Modified));
|
||||
}
|
||||
|
||||
if self.time_types.changed {
|
||||
columns.push(Column::Timestamp(TimeType::Changed));
|
||||
}
|
||||
|
||||
if self.time_types.created {
|
||||
columns.push(Column::Timestamp(TimeType::Created));
|
||||
}
|
||||
@ -81,12 +91,8 @@ impl Options {
|
||||
columns.push(Column::Timestamp(TimeType::Accessed));
|
||||
}
|
||||
|
||||
if cfg!(feature="git") {
|
||||
if let Some(d) = dir {
|
||||
if self.should_scan_for_git() && d.has_git_repo() {
|
||||
columns.push(Column::GitStatus);
|
||||
}
|
||||
}
|
||||
if cfg!(feature="git") && self.git && actually_enable_git {
|
||||
columns.push(Column::GitStatus);
|
||||
}
|
||||
|
||||
columns
|
||||
@ -98,7 +104,7 @@ impl Options {
|
||||
#[derive(Debug)]
|
||||
pub enum Column {
|
||||
Permissions,
|
||||
FileSize(SizeFormat),
|
||||
FileSize,
|
||||
Timestamp(TimeType),
|
||||
Blocks,
|
||||
User,
|
||||
@ -120,7 +126,7 @@ impl Column {
|
||||
/// Get the alignment this column should use.
|
||||
pub fn alignment(&self) -> Alignment {
|
||||
match *self {
|
||||
Column::FileSize(_)
|
||||
Column::FileSize
|
||||
| Column::HardLinks
|
||||
| Column::Inode
|
||||
| Column::Blocks
|
||||
@ -134,7 +140,7 @@ impl Column {
|
||||
pub fn header(&self) -> &'static str {
|
||||
match *self {
|
||||
Column::Permissions => "Permissions",
|
||||
Column::FileSize(_) => "Size",
|
||||
Column::FileSize => "Size",
|
||||
Column::Timestamp(t) => t.header(),
|
||||
Column::Blocks => "Blocks",
|
||||
Column::User => "User",
|
||||
@ -174,24 +180,27 @@ impl Default for SizeFormat {
|
||||
/// across most (all?) operating systems.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub enum TimeType {
|
||||
/// The file’s modified time (`st_mtime`).
|
||||
Modified,
|
||||
|
||||
/// The file’s changed time (`st_ctime`)
|
||||
Changed,
|
||||
|
||||
/// The file’s accessed time (`st_atime`).
|
||||
Accessed,
|
||||
|
||||
/// The file’s modified time (`st_mtime`).
|
||||
Modified,
|
||||
|
||||
/// The file’s creation time (`st_ctime`).
|
||||
/// The file’s creation time (`btime` or `birthtime`).
|
||||
Created,
|
||||
}
|
||||
|
||||
impl TimeType {
|
||||
|
||||
/// Returns the text to use for a column’s heading in the columns output.
|
||||
pub fn header(&self) -> &'static str {
|
||||
match *self {
|
||||
TimeType::Accessed => "Date Accessed",
|
||||
pub fn header(self) -> &'static str {
|
||||
match self {
|
||||
TimeType::Modified => "Date Modified",
|
||||
TimeType::Changed => "Date Changed",
|
||||
TimeType::Accessed => "Date Accessed",
|
||||
TimeType::Created => "Date Created",
|
||||
}
|
||||
}
|
||||
@ -205,8 +214,9 @@ impl TimeType {
|
||||
/// the time columns entirely (yet).
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct TimeTypes {
|
||||
pub accessed: bool,
|
||||
pub modified: bool,
|
||||
pub changed: bool,
|
||||
pub accessed: bool,
|
||||
pub created: bool,
|
||||
}
|
||||
|
||||
@ -215,7 +225,7 @@ impl Default for TimeTypes {
|
||||
/// By default, display just the ‘modified’ time. This is the most
|
||||
/// common option, which is why it has this shorthand.
|
||||
fn default() -> TimeTypes {
|
||||
TimeTypes { accessed: false, modified: true, created: false }
|
||||
TimeTypes { modified: true, changed: false, accessed: false, created: false }
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,15 +277,14 @@ fn determine_time_zone() -> TZResult<TimeZone> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
pub struct Table<'a> {
|
||||
columns: Vec<Column>,
|
||||
colours: &'a Colours,
|
||||
env: &'a Environment,
|
||||
widths: TableWidths,
|
||||
time_format: &'a TimeFormat,
|
||||
size_format: SizeFormat,
|
||||
git: Option<&'a GitCache>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -284,10 +293,16 @@ pub struct Row {
|
||||
}
|
||||
|
||||
impl<'a, 'f> Table<'a> {
|
||||
pub fn new(options: &'a Options, dir: Option<&'a Dir>, colours: &'a Colours) -> Table<'a> {
|
||||
let colz = options.for_dir(dir);
|
||||
let widths = TableWidths::zero(colz.len());
|
||||
Table { columns: colz, colours, env: &options.env, widths, time_format: &options.time_format }
|
||||
pub fn new(options: &'a Options, git: Option<&'a GitCache>, colours: &'a Colours) -> Table<'a> {
|
||||
let columns = options.extra_columns.collect(git.is_some());
|
||||
let widths = TableWidths::zero(columns.len());
|
||||
|
||||
Table {
|
||||
colours, widths, columns, git,
|
||||
env: &options.env,
|
||||
time_format: &options.time_format,
|
||||
size_format: options.size_format,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn widths(&self) -> &TableWidths {
|
||||
@ -318,7 +333,7 @@ impl<'a, 'f> Table<'a> {
|
||||
f::PermissionsPlus {
|
||||
file_type: file.type_char(),
|
||||
permissions: file.permissions(),
|
||||
xattrs: xattrs,
|
||||
xattrs,
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,21 +341,29 @@ impl<'a, 'f> Table<'a> {
|
||||
use output::table::TimeType::*;
|
||||
|
||||
match *column {
|
||||
Column::Permissions => self.permissions_plus(file, xattrs).render(&self.colours),
|
||||
Column::FileSize(fmt) => file.size().render(&self.colours, fmt, &self.env.numeric),
|
||||
Column::HardLinks => file.links().render(&self.colours, &self.env.numeric),
|
||||
Column::Inode => file.inode().render(&self.colours),
|
||||
Column::Blocks => file.blocks().render(&self.colours),
|
||||
Column::User => file.user().render(&self.colours, &*self.env.lock_users()),
|
||||
Column::Group => file.group().render(&self.colours, &*self.env.lock_users()),
|
||||
Column::GitStatus => file.git_status().render(&self.colours),
|
||||
Column::Permissions => self.permissions_plus(file, xattrs).render(self.colours),
|
||||
Column::FileSize => file.size().render(self.colours, self.size_format, &self.env.numeric),
|
||||
Column::HardLinks => file.links().render(self.colours, &self.env.numeric),
|
||||
Column::Inode => file.inode().render(self.colours.inode),
|
||||
Column::Blocks => file.blocks().render(self.colours),
|
||||
Column::User => file.user().render(self.colours, &*self.env.lock_users()),
|
||||
Column::Group => file.group().render(self.colours, &*self.env.lock_users()),
|
||||
Column::GitStatus => self.git_status(file).render(self.colours),
|
||||
|
||||
Column::Timestamp(Modified) => file.modified_time().render(&self.colours, &self.env.tz, &self.time_format),
|
||||
Column::Timestamp(Created) => file.created_time().render( &self.colours, &self.env.tz, &self.time_format),
|
||||
Column::Timestamp(Accessed) => file.accessed_time().render(&self.colours, &self.env.tz, &self.time_format),
|
||||
Column::Timestamp(Modified) => file.modified_time().render(self.colours.date, &self.env.tz, &self.time_format),
|
||||
Column::Timestamp(Changed) => file.changed_time() .render(self.colours.date, &self.env.tz, &self.time_format),
|
||||
Column::Timestamp(Created) => file.created_time() .render(self.colours.date, &self.env.tz, &self.time_format),
|
||||
Column::Timestamp(Accessed) => file.accessed_time().render(self.colours.date, &self.env.tz, &self.time_format),
|
||||
}
|
||||
}
|
||||
|
||||
fn git_status(&self, file: &File) -> f::Git {
|
||||
debug!("Getting Git status for file {:?}", file.path);
|
||||
self.git
|
||||
.map(|g| g.get(&file.path, file.is_directory()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn render(&self, row: Row) -> TextCell {
|
||||
let mut cell = TextCell::default();
|
||||
|
||||
@ -366,7 +389,7 @@ pub struct TableWidths(Vec<usize>);
|
||||
impl Deref for TableWidths {
|
||||
type Target = [usize];
|
||||
|
||||
fn deref<'a>(&'a self) -> &'a Self::Target {
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,57 @@
|
||||
//! Timestamp formatting.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece};
|
||||
use datetime::fmt::DateFormat;
|
||||
use locale;
|
||||
|
||||
use fs::fields::Time;
|
||||
use std::cmp;
|
||||
|
||||
|
||||
/// Every timestamp in exa needs to be rendered by a **time format**.
|
||||
/// Formatting times is tricky, because how a timestamp is rendered can
|
||||
/// depend on one or more of the following:
|
||||
///
|
||||
/// - The user’s locale, for printing the month name as “Feb”, or as “fév”,
|
||||
/// or as “2月”;
|
||||
/// - The current year, because certain formats will be less precise when
|
||||
/// dealing with dates far in the past;
|
||||
/// - The formatting style that the user asked for on the command-line.
|
||||
///
|
||||
/// Because not all formatting styles need the same data, they all have their
|
||||
/// own enum variants. It’s not worth looking the locale up if the formatter
|
||||
/// prints month names as numbers.
|
||||
///
|
||||
/// Currently exa does not support *custom* styles, where the user enters a
|
||||
/// format string in an environment variable or something. Just these four.
|
||||
#[derive(Debug)]
|
||||
pub enum TimeFormat {
|
||||
|
||||
/// The **default format** uses the user’s locale to print month names,
|
||||
/// and specifies the timestamp down to the minute for recent times, and
|
||||
/// day for older times.
|
||||
DefaultFormat(DefaultFormat),
|
||||
|
||||
/// Use the **ISO format**, which specifies the timestamp down to the
|
||||
/// minute for recent times, and day for older times. It uses a number
|
||||
/// for the month so it doesn’t need a locale.
|
||||
ISOFormat(ISOFormat),
|
||||
|
||||
/// Use the **long ISO format**, which specifies the timestamp down to the
|
||||
/// minute using only numbers, without needing the locale or year.
|
||||
LongISO,
|
||||
|
||||
/// Use the **full ISO format**, which specifies the timestamp down to the
|
||||
/// millisecond and includes its offset down to the minute. This too uses
|
||||
/// only numbers so doesn’t require any special consideration.
|
||||
FullISO,
|
||||
}
|
||||
|
||||
// There are two different formatting functions because local and zoned
|
||||
// timestamps are separate types.
|
||||
|
||||
impl TimeFormat {
|
||||
pub fn format_local(&self, time: Time) -> String {
|
||||
pub fn format_local(&self, time: Duration) -> String {
|
||||
match *self {
|
||||
TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time),
|
||||
TimeFormat::ISOFormat(ref iso) => iso.format_local(time),
|
||||
@ -22,7 +60,7 @@ impl TimeFormat {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_zoned(&self, time: Time, zone: &TimeZone) -> String {
|
||||
pub fn format_zoned(&self, time: Duration, zone: &TimeZone) -> String {
|
||||
match *self {
|
||||
TimeFormat::DefaultFormat(ref fmt) => fmt.format_zoned(time, zone),
|
||||
TimeFormat::ISOFormat(ref iso) => iso.format_zoned(time, zone),
|
||||
@ -51,7 +89,7 @@ pub struct DefaultFormat {
|
||||
}
|
||||
|
||||
impl DefaultFormat {
|
||||
pub fn new() -> DefaultFormat {
|
||||
pub fn load() -> DefaultFormat {
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
let locale = locale::Time::load_user_locale()
|
||||
@ -60,30 +98,38 @@ impl DefaultFormat {
|
||||
let current_year = LocalDateTime::now().year();
|
||||
|
||||
// Some locales use a three-character wide month name (Jan to Dec);
|
||||
// others vary between three and four (1月 to 12月). We assume that
|
||||
// December is the month with the maximum width, and use the width of
|
||||
// that to determine how to pad the other months.
|
||||
let december_width = UnicodeWidthStr::width(&*locale.short_month_name(11));
|
||||
let date_and_time = match december_width {
|
||||
4 => DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(),
|
||||
// others vary between three to four (1月 to 12月, juil.). We check each month width
|
||||
// to detect the longest and set the output format accordingly.
|
||||
let mut maximum_month_width = 0;
|
||||
for i in 0..11 {
|
||||
let current_month_width = UnicodeWidthStr::width(&*locale.short_month_name(i));
|
||||
maximum_month_width = cmp::max(maximum_month_width, current_month_width);
|
||||
}
|
||||
|
||||
let date_and_time = match maximum_month_width {
|
||||
4 => DateFormat::parse("{2>:D} {4<:M} {2>:h}:{02>:m}").unwrap(),
|
||||
5 => DateFormat::parse("{2>:D} {5<:M} {2>:h}:{02>:m}").unwrap(),
|
||||
_ => DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap(),
|
||||
};
|
||||
|
||||
let date_and_year = match december_width {
|
||||
4 => DateFormat::parse("{2>:D} {4>:M} {5>:Y}").unwrap(),
|
||||
let date_and_year = match maximum_month_width {
|
||||
4 => DateFormat::parse("{2>:D} {4<:M} {5>:Y}").unwrap(),
|
||||
5 => DateFormat::parse("{2>:D} {5<:M} {5>:Y}").unwrap(),
|
||||
_ => DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
|
||||
};
|
||||
|
||||
DefaultFormat { current_year, locale, date_and_time, date_and_year }
|
||||
}
|
||||
}
|
||||
|
||||
impl DefaultFormat {
|
||||
fn is_recent(&self, date: LocalDateTime) -> bool {
|
||||
date.year() == self.current_year
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn format_local(&self, time: Time) -> String {
|
||||
let date = LocalDateTime::at(time.seconds as i64);
|
||||
fn format_local(&self, time: Duration) -> String {
|
||||
let date = LocalDateTime::at(time.as_secs() as i64);
|
||||
|
||||
if self.is_recent(date) {
|
||||
self.date_and_time.format(&date, &self.locale)
|
||||
@ -94,8 +140,8 @@ impl DefaultFormat {
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn format_zoned(&self, time: Time, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64));
|
||||
fn format_zoned(&self, time: Duration, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64));
|
||||
|
||||
if self.is_recent(date) {
|
||||
self.date_and_time.format(&date, &self.locale)
|
||||
@ -108,16 +154,16 @@ impl DefaultFormat {
|
||||
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn long_local(time: Time) -> String {
|
||||
let date = LocalDateTime::at(time.seconds as i64);
|
||||
fn long_local(time: Duration) -> String {
|
||||
let date = LocalDateTime::at(time.as_secs() as i64);
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute())
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn long_zoned(time: Time, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64));
|
||||
fn long_zoned(time: Duration, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64));
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute())
|
||||
@ -125,23 +171,23 @@ fn long_zoned(time: Time, zone: &TimeZone) -> String {
|
||||
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn full_local(time: Time) -> String {
|
||||
let date = LocalDateTime::at(time.seconds as i64);
|
||||
fn full_local(time: Duration) -> String {
|
||||
let date = LocalDateTime::at(time.as_secs() as i64);
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute(), date.second(), time.nanoseconds)
|
||||
date.hour(), date.minute(), date.second(), time.subsec_nanos())
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn full_zoned(time: Time, zone: &TimeZone) -> String {
|
||||
fn full_zoned(time: Duration, zone: &TimeZone) -> String {
|
||||
use datetime::Offset;
|
||||
|
||||
let local = LocalDateTime::at(time.seconds as i64);
|
||||
let local = LocalDateTime::at(time.as_secs() as i64);
|
||||
let date = zone.to_zoned(local);
|
||||
let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute(), date.second(), time.nanoseconds,
|
||||
date.hour(), date.minute(), date.second(), time.subsec_nanos(),
|
||||
offset.hours(), offset.minutes().abs())
|
||||
}
|
||||
|
||||
@ -156,42 +202,44 @@ pub struct ISOFormat {
|
||||
}
|
||||
|
||||
impl ISOFormat {
|
||||
pub fn new() -> Self {
|
||||
pub fn load() -> ISOFormat {
|
||||
let current_year = LocalDateTime::now().year();
|
||||
ISOFormat { current_year }
|
||||
}
|
||||
}
|
||||
|
||||
impl ISOFormat {
|
||||
fn is_recent(&self, date: LocalDateTime) -> bool {
|
||||
date.year() == self.current_year
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn format_local(&self, time: Time) -> String {
|
||||
let date = LocalDateTime::at(time.seconds as i64);
|
||||
fn format_local(&self, time: Duration) -> String {
|
||||
let date = LocalDateTime::at(time.as_secs() as i64);
|
||||
|
||||
if self.is_recent(date) {
|
||||
format!("{:04}-{:02}-{:02}",
|
||||
date.year(), date.month() as usize, date.day())
|
||||
}
|
||||
else {
|
||||
format!("{:02}-{:02} {:02}:{:02}",
|
||||
date.month() as usize, date.day(),
|
||||
date.hour(), date.minute())
|
||||
}
|
||||
else {
|
||||
format!("{:04}-{:02}-{:02}",
|
||||
date.year(), date.month() as usize, date.day())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn format_zoned(&self, time: Time, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64));
|
||||
fn format_zoned(&self, time: Duration, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64));
|
||||
|
||||
if self.is_recent(date) {
|
||||
format!("{:04}-{:02}-{:02}",
|
||||
date.year(), date.month() as usize, date.day())
|
||||
}
|
||||
else {
|
||||
format!("{:02}-{:02} {:02}:{:02}",
|
||||
date.month() as usize, date.day(),
|
||||
date.hour(), date.minute())
|
||||
}
|
||||
else {
|
||||
format!("{:04}-{:02}-{:02}",
|
||||
date.year(), date.month() as usize, date.day())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
406
src/style/colours.rs
Normal file
406
src/style/colours.rs
Normal file
@ -0,0 +1,406 @@
|
||||
use ansi_term::Style;
|
||||
use ansi_term::Colour::{Red, Green, Yellow, Blue, Cyan, Purple, Fixed};
|
||||
|
||||
use output::render;
|
||||
use output::file_name::Colours as FileNameColours;
|
||||
|
||||
use style::lsc::Pair;
|
||||
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Colours {
|
||||
pub colourful: bool,
|
||||
pub scale: bool,
|
||||
|
||||
pub filekinds: FileKinds,
|
||||
pub perms: Permissions,
|
||||
pub size: Size,
|
||||
pub users: Users,
|
||||
pub links: Links,
|
||||
pub git: Git,
|
||||
|
||||
pub punctuation: Style,
|
||||
pub date: Style,
|
||||
pub inode: Style,
|
||||
pub blocks: Style,
|
||||
pub header: Style,
|
||||
|
||||
pub symlink_path: Style,
|
||||
pub control_char: Style,
|
||||
pub broken_symlink: Style,
|
||||
pub broken_path_overlay: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct FileKinds {
|
||||
pub normal: Style,
|
||||
pub directory: Style,
|
||||
pub symlink: Style,
|
||||
pub pipe: Style,
|
||||
pub block_device: Style,
|
||||
pub char_device: Style,
|
||||
pub socket: Style,
|
||||
pub special: Style,
|
||||
pub executable: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Permissions {
|
||||
pub user_read: Style,
|
||||
pub user_write: Style,
|
||||
pub user_execute_file: Style,
|
||||
pub user_execute_other: Style,
|
||||
|
||||
pub group_read: Style,
|
||||
pub group_write: Style,
|
||||
pub group_execute: Style,
|
||||
|
||||
pub other_read: Style,
|
||||
pub other_write: Style,
|
||||
pub other_execute: Style,
|
||||
|
||||
pub special_user_file: Style,
|
||||
pub special_other: Style,
|
||||
|
||||
pub attribute: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Size {
|
||||
pub numbers: Style,
|
||||
pub unit: Style,
|
||||
|
||||
pub major: Style,
|
||||
pub minor: Style,
|
||||
|
||||
pub scale_byte: Style,
|
||||
pub scale_kilo: Style,
|
||||
pub scale_mega: Style,
|
||||
pub scale_giga: Style,
|
||||
pub scale_huge: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Users {
|
||||
pub user_you: Style,
|
||||
pub user_someone_else: Style,
|
||||
pub group_yours: Style,
|
||||
pub group_not_yours: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Links {
|
||||
pub normal: Style,
|
||||
pub multi_link_file: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Git {
|
||||
pub new: Style,
|
||||
pub modified: Style,
|
||||
pub deleted: Style,
|
||||
pub renamed: Style,
|
||||
pub typechange: Style,
|
||||
pub ignored: Style,
|
||||
}
|
||||
|
||||
impl Colours {
|
||||
pub fn plain() -> Colours {
|
||||
Colours::default()
|
||||
}
|
||||
|
||||
pub fn colourful(scale: bool) -> Colours {
|
||||
Colours {
|
||||
colourful: true,
|
||||
scale,
|
||||
|
||||
filekinds: FileKinds {
|
||||
normal: Style::default(),
|
||||
directory: Blue.bold(),
|
||||
symlink: Cyan.normal(),
|
||||
pipe: Yellow.normal(),
|
||||
block_device: Yellow.bold(),
|
||||
char_device: Yellow.bold(),
|
||||
socket: Red.bold(),
|
||||
special: Yellow.normal(),
|
||||
executable: Green.bold(),
|
||||
},
|
||||
|
||||
perms: Permissions {
|
||||
user_read: Yellow.bold(),
|
||||
user_write: Red.bold(),
|
||||
user_execute_file: Green.bold().underline(),
|
||||
user_execute_other: Green.bold(),
|
||||
|
||||
group_read: Yellow.normal(),
|
||||
group_write: Red.normal(),
|
||||
group_execute: Green.normal(),
|
||||
|
||||
other_read: Yellow.normal(),
|
||||
other_write: Red.normal(),
|
||||
other_execute: Green.normal(),
|
||||
|
||||
special_user_file: Purple.normal(),
|
||||
special_other: Purple.normal(),
|
||||
|
||||
attribute: Style::default(),
|
||||
},
|
||||
|
||||
size: Size {
|
||||
numbers: Green.bold(),
|
||||
unit: Green.normal(),
|
||||
|
||||
major: Green.bold(),
|
||||
minor: Green.normal(),
|
||||
|
||||
scale_byte: Fixed(118).normal(),
|
||||
scale_kilo: Fixed(190).normal(),
|
||||
scale_mega: Fixed(226).normal(),
|
||||
scale_giga: Fixed(220).normal(),
|
||||
scale_huge: Fixed(214).normal(),
|
||||
},
|
||||
|
||||
users: Users {
|
||||
user_you: Yellow.bold(),
|
||||
user_someone_else: Style::default(),
|
||||
group_yours: Yellow.bold(),
|
||||
group_not_yours: Style::default(),
|
||||
},
|
||||
|
||||
links: Links {
|
||||
normal: Red.bold(),
|
||||
multi_link_file: Red.on(Yellow),
|
||||
},
|
||||
|
||||
git: Git {
|
||||
new: Green.normal(),
|
||||
modified: Blue.normal(),
|
||||
deleted: Red.normal(),
|
||||
renamed: Yellow.normal(),
|
||||
typechange: Purple.normal(),
|
||||
ignored: Style::default().dimmed(),
|
||||
},
|
||||
|
||||
punctuation: Fixed(244).normal(),
|
||||
date: Blue.normal(),
|
||||
inode: Purple.normal(),
|
||||
blocks: Cyan.normal(),
|
||||
header: Style::default().underline(),
|
||||
|
||||
symlink_path: Cyan.normal(),
|
||||
control_char: Red.normal(),
|
||||
broken_symlink: Red.normal(),
|
||||
broken_path_overlay: Style::default().underline(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Some of the styles are **overlays**: although they have the same attribute
|
||||
/// set as regular styles (foreground and background colours, bold, underline,
|
||||
/// etc), they’re intended to be used to *amend* existing styles.
|
||||
///
|
||||
/// For example, the target path of a broken symlink is displayed in a red,
|
||||
/// underlined style by default. Paths can contain control characters, so
|
||||
/// these control characters need to be underlined too, otherwise it looks
|
||||
/// weird. So instead of having four separate configurable styles for “link
|
||||
/// path”, “broken link path”, “control character” and “broken control
|
||||
/// character”, there are styles for “link path”, “control character”, and
|
||||
/// “broken link overlay”, the latter of which is just set to override the
|
||||
/// underline attribute on the other two.
|
||||
fn apply_overlay(mut base: Style, overlay: Style) -> Style {
|
||||
if let Some(fg) = overlay.foreground { base.foreground = Some(fg); }
|
||||
if let Some(bg) = overlay.background { base.background = Some(bg); }
|
||||
|
||||
if overlay.is_bold { base.is_bold = true; }
|
||||
if overlay.is_dimmed { base.is_dimmed = true; }
|
||||
if overlay.is_italic { base.is_italic = true; }
|
||||
if overlay.is_underline { base.is_underline = true; }
|
||||
if overlay.is_blink { base.is_blink = true; }
|
||||
if overlay.is_reverse { base.is_reverse = true; }
|
||||
if overlay.is_hidden { base.is_hidden = true; }
|
||||
if overlay.is_strikethrough { base.is_strikethrough = true; }
|
||||
|
||||
base
|
||||
}
|
||||
// TODO: move this function to the ansi_term crate
|
||||
|
||||
|
||||
impl Colours {
|
||||
|
||||
/// Sets a value on this set of colours using one of the keys understood
|
||||
/// by the `LS_COLORS` environment variable. Invalid keys set nothing, but
|
||||
/// return false.
|
||||
pub fn set_ls(&mut self, pair: &Pair) -> bool {
|
||||
match pair.key {
|
||||
"di" => self.filekinds.directory = pair.to_style(), // DIR
|
||||
"ex" => self.filekinds.executable = pair.to_style(), // EXEC
|
||||
"fi" => self.filekinds.normal = pair.to_style(), // FILE
|
||||
"pi" => self.filekinds.pipe = pair.to_style(), // FIFO
|
||||
"so" => self.filekinds.socket = pair.to_style(), // SOCK
|
||||
"bd" => self.filekinds.block_device = pair.to_style(), // BLK
|
||||
"cd" => self.filekinds.char_device = pair.to_style(), // CHR
|
||||
"ln" => self.filekinds.symlink = pair.to_style(), // LINK
|
||||
"or" => self.broken_symlink = pair.to_style(), // ORPHAN
|
||||
_ => return false,
|
||||
// Codes we don’t do anything with:
|
||||
// MULTIHARDLINK, DOOR, SETUID, SETGID, CAPABILITY,
|
||||
// STICKY_OTHER_WRITABLE, OTHER_WRITABLE, STICKY, MISSING
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Sets a value on this set of colours using one of the keys understood
|
||||
/// by the `EXA_COLORS` environment variable. Invalid keys set nothing,
|
||||
/// but return false. This doesn’t take the `LS_COLORS` keys into account,
|
||||
/// so `set_ls` should have been run first.
|
||||
pub fn set_exa(&mut self, pair: &Pair) -> bool {
|
||||
match pair.key {
|
||||
"ur" => self.perms.user_read = pair.to_style(),
|
||||
"uw" => self.perms.user_write = pair.to_style(),
|
||||
"ux" => self.perms.user_execute_file = pair.to_style(),
|
||||
"ue" => self.perms.user_execute_other = pair.to_style(),
|
||||
"gr" => self.perms.group_read = pair.to_style(),
|
||||
"gw" => self.perms.group_write = pair.to_style(),
|
||||
"gx" => self.perms.group_execute = pair.to_style(),
|
||||
"tr" => self.perms.other_read = pair.to_style(),
|
||||
"tw" => self.perms.other_write = pair.to_style(),
|
||||
"tx" => self.perms.other_execute = pair.to_style(),
|
||||
"su" => self.perms.special_user_file = pair.to_style(),
|
||||
"sf" => self.perms.special_other = pair.to_style(),
|
||||
"xa" => self.perms.attribute = pair.to_style(),
|
||||
|
||||
"sn" => self.size.numbers = pair.to_style(),
|
||||
"sb" => self.size.unit = pair.to_style(),
|
||||
"df" => self.size.major = pair.to_style(),
|
||||
"ds" => self.size.minor = pair.to_style(),
|
||||
|
||||
"uu" => self.users.user_you = pair.to_style(),
|
||||
"un" => self.users.user_someone_else = pair.to_style(),
|
||||
"gu" => self.users.group_yours = pair.to_style(),
|
||||
"gn" => self.users.group_not_yours = pair.to_style(),
|
||||
|
||||
"lc" => self.links.normal = pair.to_style(),
|
||||
"lm" => self.links.multi_link_file = pair.to_style(),
|
||||
|
||||
"ga" => self.git.new = pair.to_style(),
|
||||
"gm" => self.git.modified = pair.to_style(),
|
||||
"gd" => self.git.deleted = pair.to_style(),
|
||||
"gv" => self.git.renamed = pair.to_style(),
|
||||
"gt" => self.git.typechange = pair.to_style(),
|
||||
|
||||
"xx" => self.punctuation = pair.to_style(),
|
||||
"da" => self.date = pair.to_style(),
|
||||
"in" => self.inode = pair.to_style(),
|
||||
"bl" => self.blocks = pair.to_style(),
|
||||
"hd" => self.header = pair.to_style(),
|
||||
"lp" => self.symlink_path = pair.to_style(),
|
||||
"cc" => self.control_char = pair.to_style(),
|
||||
"bO" => self.broken_path_overlay = pair.to_style(),
|
||||
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl render::BlocksColours for Colours {
|
||||
fn block_count(&self) -> Style { self.blocks }
|
||||
fn no_blocks(&self) -> Style { self.punctuation }
|
||||
}
|
||||
|
||||
impl render::FiletypeColours for Colours {
|
||||
fn normal(&self) -> Style { self.filekinds.normal }
|
||||
fn directory(&self) -> Style { self.filekinds.directory }
|
||||
fn pipe(&self) -> Style { self.filekinds.pipe }
|
||||
fn symlink(&self) -> Style { self.filekinds.symlink }
|
||||
fn block_device(&self) -> Style { self.filekinds.block_device }
|
||||
fn char_device(&self) -> Style { self.filekinds.char_device }
|
||||
fn socket(&self) -> Style { self.filekinds.socket }
|
||||
fn special(&self) -> Style { self.filekinds.special }
|
||||
}
|
||||
|
||||
impl render::GitColours for Colours {
|
||||
fn not_modified(&self) -> Style { self.punctuation }
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
fn new(&self) -> Style { self.git.new }
|
||||
fn modified(&self) -> Style { self.git.modified }
|
||||
fn deleted(&self) -> Style { self.git.deleted }
|
||||
fn renamed(&self) -> Style { self.git.renamed }
|
||||
fn type_change(&self) -> Style { self.git.typechange }
|
||||
fn ignored(&self) -> Style { self.git.ignored }
|
||||
}
|
||||
|
||||
impl render::GroupColours for Colours {
|
||||
fn yours(&self) -> Style { self.users.group_yours }
|
||||
fn not_yours(&self) -> Style { self.users.group_not_yours }
|
||||
}
|
||||
|
||||
impl render::LinksColours for Colours {
|
||||
fn normal(&self) -> Style { self.links.normal }
|
||||
fn multi_link_file(&self) -> Style { self.links.multi_link_file }
|
||||
}
|
||||
|
||||
impl render::PermissionsColours for Colours {
|
||||
fn dash(&self) -> Style { self.punctuation }
|
||||
fn user_read(&self) -> Style { self.perms.user_read }
|
||||
fn user_write(&self) -> Style { self.perms.user_write }
|
||||
fn user_execute_file(&self) -> Style { self.perms.user_execute_file }
|
||||
fn user_execute_other(&self) -> Style { self.perms.user_execute_other }
|
||||
fn group_read(&self) -> Style { self.perms.group_read }
|
||||
fn group_write(&self) -> Style { self.perms.group_write }
|
||||
fn group_execute(&self) -> Style { self.perms.group_execute }
|
||||
fn other_read(&self) -> Style { self.perms.other_read }
|
||||
fn other_write(&self) -> Style { self.perms.other_write }
|
||||
fn other_execute(&self) -> Style { self.perms.other_execute }
|
||||
fn special_user_file(&self) -> Style { self.perms.special_user_file }
|
||||
fn special_other(&self) -> Style { self.perms.special_other }
|
||||
fn attribute(&self) -> Style { self.perms.attribute }
|
||||
}
|
||||
|
||||
impl render::SizeColours for Colours {
|
||||
fn size(&self, size: u64) -> Style {
|
||||
if self.scale {
|
||||
if size < 1024 {
|
||||
self.size.scale_byte
|
||||
}
|
||||
else if size < 1024 * 1024 {
|
||||
self.size.scale_kilo
|
||||
}
|
||||
else if size < 1024 * 1024 * 1024 {
|
||||
self.size.scale_mega
|
||||
}
|
||||
else if size < 1024 * 1024 * 1024 * 1024 {
|
||||
self.size.scale_giga
|
||||
}
|
||||
else {
|
||||
self.size.scale_huge
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.size.numbers
|
||||
}
|
||||
}
|
||||
|
||||
fn unit(&self) -> Style { self.size.unit }
|
||||
fn no_size(&self) -> Style { self.punctuation }
|
||||
fn major(&self) -> Style { self.size.major }
|
||||
fn comma(&self) -> Style { self.punctuation }
|
||||
fn minor(&self) -> Style { self.size.minor }
|
||||
}
|
||||
|
||||
impl render::UserColours for Colours {
|
||||
fn you(&self) -> Style { self.users.user_you }
|
||||
fn someone_else(&self) -> Style { self.users.user_someone_else }
|
||||
}
|
||||
|
||||
impl FileNameColours for Colours {
|
||||
fn normal_arrow(&self) -> Style { self.punctuation }
|
||||
fn broken_symlink(&self) -> Style { self.broken_symlink }
|
||||
fn broken_filename(&self) -> Style { apply_overlay(self.broken_symlink, self.broken_path_overlay) }
|
||||
fn broken_control_char(&self) -> Style { apply_overlay(self.control_char, self.broken_path_overlay) }
|
||||
fn control_char(&self) -> Style { self.control_char }
|
||||
fn symlink_path(&self) -> Style { self.symlink_path }
|
||||
fn executable_file(&self) -> Style { self.filekinds.executable }
|
||||
}
|
228
src/style/lsc.rs
Normal file
228
src/style/lsc.rs
Normal file
@ -0,0 +1,228 @@
|
||||
use std::ops::FnMut;
|
||||
|
||||
use ansi_term::{Colour, Style};
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
|
||||
// Parsing the LS_COLORS environment variable into a map of names to Style values.
|
||||
//
|
||||
// This is sitting around undocumented at the moment because it’s a feature
|
||||
// that should really be unnecessary! exa highlights its output by creating a
|
||||
// theme of one Style value per part of the interface that can be coloured,
|
||||
// then reading styles from that theme. The LS_COLORS variable, on the other
|
||||
// hand, can contain arbitrary characters that ls is supposed to add to the
|
||||
// output, without needing to know what they actually do. This puts exa in the
|
||||
// annoying position of having to parse the ANSI escape codes _back_ into
|
||||
// Style values before it’s able to use them. Doing this has a lot of
|
||||
// downsides: if a new terminal feature is added with its own code, exa won’t
|
||||
// be able to use this without explicit support for parsing the feature, while
|
||||
// ls would not even need to know it existed. And there are some edge cases in
|
||||
// ANSI codes, where terminals would accept codes exa is strict about it. It’s
|
||||
// just not worth doing, and there should really be a way to just use slices
|
||||
// of the LS_COLORS string without having to parse them.
|
||||
|
||||
|
||||
pub struct LSColors<'var>(pub &'var str);
|
||||
|
||||
impl<'var> LSColors<'var> {
|
||||
pub fn each_pair<C>(&mut self, mut callback: C) where C: FnMut(Pair<'var>) -> () {
|
||||
for next in self.0.split(':') {
|
||||
let bits = next.split('=')
|
||||
.take(3)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if bits.len() == 2 && !bits[0].is_empty() && !bits[1].is_empty() {
|
||||
callback(Pair { key: bits[0], value: bits[1] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Pair<'var> {
|
||||
pub key: &'var str,
|
||||
pub value: &'var str,
|
||||
}
|
||||
|
||||
use std::iter::Peekable;
|
||||
fn parse_into_high_colour<'a, I>(iter: &mut Peekable<I>) -> Option<Colour>
|
||||
where I: Iterator<Item=&'a str> {
|
||||
match iter.peek() {
|
||||
Some(&"5") => {
|
||||
let _ = iter.next();
|
||||
if let Some(byte) = iter.next() {
|
||||
if let Ok(num) = byte.parse() {
|
||||
return Some(Fixed(num));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(&"2") => {
|
||||
let _ = iter.next();
|
||||
if let Some(hexes) = iter.next() {
|
||||
// Some terminals support R:G:B instead of R;G;B
|
||||
// but this clashes with splitting on ':' in each_pair above.
|
||||
/*if hexes.contains(':') {
|
||||
let rgb = hexes.splitn(3, ':').collect::<Vec<_>>();
|
||||
if rgb.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
else if let (Ok(r), Ok(g), Ok(b)) = (rgb[0].parse(), rgb[1].parse(), rgb[2].parse()) {
|
||||
return Some(RGB(r, g, b));
|
||||
}
|
||||
}*/
|
||||
|
||||
if let (Some(r), Some(g), Some(b)) = (hexes.parse().ok(),
|
||||
iter.next().and_then(|s| s.parse().ok()),
|
||||
iter.next().and_then(|s| s.parse().ok())) {
|
||||
return Some(RGB(r, g, b));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl<'var> Pair<'var> {
|
||||
pub fn to_style(&self) -> Style {
|
||||
let mut style = Style::default();
|
||||
let mut iter = self.value.split(';').peekable();
|
||||
|
||||
while let Some(num) = iter.next() {
|
||||
match num.trim_start_matches('0') {
|
||||
|
||||
// Bold and italic
|
||||
"1" => style = style.bold(),
|
||||
"2" => style = style.dimmed(),
|
||||
"3" => style = style.italic(),
|
||||
"4" => style = style.underline(),
|
||||
"5" => style = style.blink(),
|
||||
// 6 is supposedly a faster blink
|
||||
"7" => style = style.reverse(),
|
||||
"8" => style = style.hidden(),
|
||||
"9" => style = style.strikethrough(),
|
||||
|
||||
// Foreground colours
|
||||
"30" => style = style.fg(Black),
|
||||
"31" => style = style.fg(Red),
|
||||
"32" => style = style.fg(Green),
|
||||
"33" => style = style.fg(Yellow),
|
||||
"34" => style = style.fg(Blue),
|
||||
"35" => style = style.fg(Purple),
|
||||
"36" => style = style.fg(Cyan),
|
||||
"37" => style = style.fg(White),
|
||||
"38" => if let Some(c) = parse_into_high_colour(&mut iter) { style = style.fg(c) },
|
||||
|
||||
// Background colours
|
||||
"40" => style = style.on(Black),
|
||||
"41" => style = style.on(Red),
|
||||
"42" => style = style.on(Green),
|
||||
"43" => style = style.on(Yellow),
|
||||
"44" => style = style.on(Blue),
|
||||
"45" => style = style.on(Purple),
|
||||
"46" => style = style.on(Cyan),
|
||||
"47" => style = style.on(White),
|
||||
"48" => if let Some(c) = parse_into_high_colour(&mut iter) { style = style.on(c) },
|
||||
|
||||
_ => {/* ignore the error and do nothing */},
|
||||
}
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod ansi_test {
|
||||
use super::*;
|
||||
use ansi_term::Style;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $input:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
assert_eq!(Pair { key: "", value: $input }.to_style(), $result);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Styles
|
||||
test!(bold: "1" => Style::default().bold());
|
||||
test!(bold2: "01" => Style::default().bold());
|
||||
test!(under: "4" => Style::default().underline());
|
||||
test!(unde2: "04" => Style::default().underline());
|
||||
test!(both: "1;4" => Style::default().bold().underline());
|
||||
test!(both2: "01;04" => Style::default().bold().underline());
|
||||
test!(fg: "31" => Red.normal());
|
||||
test!(bg: "43" => Style::default().on(Yellow));
|
||||
test!(bfg: "31;43" => Red.on(Yellow));
|
||||
test!(bfg2: "0031;0043" => Red.on(Yellow));
|
||||
test!(all: "43;31;1;4" => Red.on(Yellow).bold().underline());
|
||||
test!(again: "1;1;1;1;1" => Style::default().bold());
|
||||
|
||||
// Failure cases
|
||||
test!(empty: "" => Style::default());
|
||||
test!(semis: ";;;;;;" => Style::default());
|
||||
test!(nines: "99999999" => Style::default());
|
||||
test!(word: "GREEN" => Style::default());
|
||||
|
||||
// Higher colours
|
||||
test!(hifg: "38;5;149" => Fixed(149).normal());
|
||||
test!(hibg: "48;5;1" => Style::default().on(Fixed(1)));
|
||||
test!(hibo: "48;5;1;1" => Style::default().on(Fixed(1)).bold());
|
||||
test!(hiund: "4;48;5;1" => Style::default().on(Fixed(1)).underline());
|
||||
|
||||
test!(rgb: "38;2;255;100;0" => Style::default().fg(RGB(255, 100, 0)));
|
||||
test!(rgbi: "38;2;255;100;0;3" => Style::default().fg(RGB(255, 100, 0)).italic());
|
||||
test!(rgbbg: "48;2;255;100;0" => Style::default().on(RGB(255, 100, 0)));
|
||||
test!(rgbbi: "48;2;255;100;0;3" => Style::default().on(RGB(255, 100, 0)).italic());
|
||||
|
||||
test!(fgbg: "38;5;121;48;5;212" => Fixed(121).on(Fixed(212)));
|
||||
test!(bgfg: "48;5;121;38;5;212" => Fixed(212).on(Fixed(121)));
|
||||
test!(toohi: "48;5;999" => Style::default());
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $input:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut lscs = Vec::new();
|
||||
LSColors($input).each_pair(|p| lscs.push( (p.key.clone(), p.to_style()) ));
|
||||
assert_eq!(lscs, $result.to_vec());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Bad parses
|
||||
test!(empty: "" => []);
|
||||
test!(jibber: "blah" => []);
|
||||
|
||||
test!(equals: "=" => []);
|
||||
test!(starts: "=di" => []);
|
||||
test!(ends: "id=" => []);
|
||||
|
||||
// Foreground colours
|
||||
test!(green: "cb=32" => [ ("cb", Green.normal()) ]);
|
||||
test!(red: "di=31" => [ ("di", Red.normal()) ]);
|
||||
test!(blue: "la=34" => [ ("la", Blue.normal()) ]);
|
||||
|
||||
// Background colours
|
||||
test!(yellow: "do=43" => [ ("do", Style::default().on(Yellow)) ]);
|
||||
test!(purple: "re=45" => [ ("re", Style::default().on(Purple)) ]);
|
||||
test!(cyan: "mi=46" => [ ("mi", Style::default().on(Cyan)) ]);
|
||||
|
||||
// Bold and underline
|
||||
test!(bold: "fa=1" => [ ("fa", Style::default().bold()) ]);
|
||||
test!(under: "so=4" => [ ("so", Style::default().underline()) ]);
|
||||
test!(both: "la=1;4" => [ ("la", Style::default().bold().underline()) ]);
|
||||
|
||||
// More and many
|
||||
test!(more: "me=43;21;55;34:yu=1;4;1" => [ ("me", Blue.on(Yellow)), ("yu", Style::default().bold().underline()) ]);
|
||||
test!(many: "red=31:green=32:blue=34" => [ ("red", Red.normal()), ("green", Green.normal()), ("blue", Blue.normal()) ]);
|
||||
}
|
5
src/style/mod.rs
Normal file
5
src/style/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod colours;
|
||||
pub use self::colours::Colours;
|
||||
|
||||
mod lsc;
|
||||
pub use self::lsc::LSColors;
|
64
src/term.rs
64
src/term.rs
@ -1,64 +0,0 @@
|
||||
//! System calls for getting the terminal size.
|
||||
//!
|
||||
//! Getting the terminal size is performed using an ioctl command that takes
|
||||
//! the file handle to the terminal -- which in this case, is stdout -- and
|
||||
//! populates a structure containing the values.
|
||||
//!
|
||||
//! The size is needed when the user wants the output formatted into columns:
|
||||
//! the default grid view, or the hybrid grid-details view.
|
||||
|
||||
use std::mem::zeroed;
|
||||
use libc::{c_int, c_ushort, c_ulong, STDOUT_FILENO};
|
||||
|
||||
|
||||
/// The number of rows and columns of a terminal.
|
||||
struct Winsize {
|
||||
ws_row: c_ushort,
|
||||
ws_col: c_ushort,
|
||||
}
|
||||
|
||||
// Unfortunately the actual command is not standardised...
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
static TIOCGWINSZ: c_ulong = 0x5413;
|
||||
|
||||
#[cfg(any(target_os = "macos",
|
||||
target_os = "ios",
|
||||
target_os = "bitrig",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"))]
|
||||
static TIOCGWINSZ: c_ulong = 0x40087468;
|
||||
|
||||
extern {
|
||||
pub fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
|
||||
}
|
||||
|
||||
/// Runs the ioctl command. Returns (0, 0) if output is not to a terminal, or
|
||||
/// there is an error. (0, 0) is an invalid size to have anyway, which is why
|
||||
/// it can be used as a nil value.
|
||||
unsafe fn get_dimensions() -> Winsize {
|
||||
let mut window: Winsize = zeroed();
|
||||
let result = ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut window);
|
||||
|
||||
if result == -1 {
|
||||
zeroed()
|
||||
}
|
||||
else {
|
||||
window
|
||||
}
|
||||
}
|
||||
|
||||
/// Query the current processes's output, returning its width and height as a
|
||||
/// number of characters. Returns `None` if the output isn't to a terminal.
|
||||
pub fn dimensions() -> Option<(usize, usize)> {
|
||||
let w = unsafe { get_dimensions() };
|
||||
|
||||
if w.ws_col == 0 || w.ws_row == 0 {
|
||||
None
|
||||
}
|
||||
else {
|
||||
Some((w.ws_col as usize, w.ws_row as usize))
|
||||
}
|
||||
}
|
57
xtests/attributes_dir
Normal file
57
xtests/attributes_dir
Normal file
@ -0,0 +1,57 @@
|
||||
[36m/testcases/[1;34mattributes[0m
|
||||
[38;5;244m├──[0m [1;34mdirs[0m
|
||||
[38;5;244m│ ├──[0m [1;34mno-xattrs_empty[0m
|
||||
[38;5;244m│ ├──[0m [1;34mno-xattrs_empty_forbidden[0m
|
||||
[38;5;244m│ │ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m│ ├──[0m [1;34mno-xattrs_one-file[0m
|
||||
[38;5;244m│ │ └──[0m file-in-question
|
||||
[38;5;244m│ ├──[0m [1;34mno-xattrs_one-file_forbidden[0m
|
||||
[38;5;244m│ │ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m│ ├──[0m [1;34mno-xattrs_two-files[0m
|
||||
[38;5;244m│ │ ├──[0m that-file
|
||||
[38;5;244m│ │ └──[0m this-file
|
||||
[38;5;244m│ ├──[0m [1;34mno-xattrs_two-files_forbidden[0m
|
||||
[38;5;244m│ │ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m│ ├──[0m [1;34mone-xattr_empty[0m
|
||||
[38;5;244m│ │ └──[0m user.greeting (len 5)
|
||||
[38;5;244m│ ├──[0m [1;34mone-xattr_empty_forbidden[0m
|
||||
[38;5;244m│ │ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m│ ├──[0m [1;34mone-xattr_one-file[0m
|
||||
[38;5;244m│ │ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ │ └──[0m file-in-question
|
||||
[38;5;244m│ ├──[0m [1;34mone-xattr_one-file_forbidden[0m
|
||||
[38;5;244m│ │ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m│ ├──[0m [1;34mone-xattr_two-files[0m
|
||||
[38;5;244m│ │ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ │ ├──[0m that-file
|
||||
[38;5;244m│ │ └──[0m this-file
|
||||
[38;5;244m│ ├──[0m [1;34mone-xattr_two-files_forbidden[0m
|
||||
[38;5;244m│ │ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m│ ├──[0m [1;34mtwo-xattrs_empty[0m
|
||||
[38;5;244m│ │ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ │ └──[0m user.another_greeting (len 2)
|
||||
[38;5;244m│ ├──[0m [1;34mtwo-xattrs_empty_forbidden[0m
|
||||
[38;5;244m│ │ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m│ ├──[0m [1;34mtwo-xattrs_one-file[0m
|
||||
[38;5;244m│ │ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ │ ├──[0m user.another_greeting (len 2)
|
||||
[38;5;244m│ │ └──[0m file-in-question
|
||||
[38;5;244m│ ├──[0m [1;34mtwo-xattrs_one-file_forbidden[0m
|
||||
[38;5;244m│ │ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m│ ├──[0m [1;34mtwo-xattrs_two-files[0m
|
||||
[38;5;244m│ │ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ │ ├──[0m user.another_greeting (len 2)
|
||||
[38;5;244m│ │ ├──[0m that-file
|
||||
[38;5;244m│ │ └──[0m this-file
|
||||
[38;5;244m│ └──[0m [1;34mtwo-xattrs_two-files_forbidden[0m
|
||||
[38;5;244m│ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m└──[0m [1;34mfiles[0m
|
||||
[38;5;244m ├──[0m no-xattrs
|
||||
[38;5;244m ├──[0m no-xattrs_forbidden
|
||||
[38;5;244m ├──[0m one-xattr
|
||||
[38;5;244m │ └──[0m user.greeting (len 5)
|
||||
[38;5;244m ├──[0m one-xattr_forbidden
|
||||
[38;5;244m ├──[0m two-xattrs
|
||||
[38;5;244m │ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m │ └──[0m user.another_greeting (len 2)
|
||||
[38;5;244m └──[0m two-xattrs_forbidden
|
56
xtests/attributes_files
Normal file
56
xtests/attributes_files
Normal file
@ -0,0 +1,56 @@
|
||||
[36m/testcases/attributes/[1;34mdirs[0m
|
||||
[38;5;244m├──[0m [1;34mno-xattrs_empty[0m
|
||||
[38;5;244m├──[0m [1;34mno-xattrs_empty_forbidden[0m
|
||||
[38;5;244m│ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m├──[0m [1;34mno-xattrs_one-file[0m
|
||||
[38;5;244m│ └──[0m file-in-question
|
||||
[38;5;244m├──[0m [1;34mno-xattrs_one-file_forbidden[0m
|
||||
[38;5;244m│ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m├──[0m [1;34mno-xattrs_two-files[0m
|
||||
[38;5;244m│ ├──[0m that-file
|
||||
[38;5;244m│ └──[0m this-file
|
||||
[38;5;244m├──[0m [1;34mno-xattrs_two-files_forbidden[0m
|
||||
[38;5;244m│ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m├──[0m [1;34mone-xattr_empty[0m
|
||||
[38;5;244m│ └──[0m user.greeting (len 5)
|
||||
[38;5;244m├──[0m [1;34mone-xattr_empty_forbidden[0m
|
||||
[38;5;244m│ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m├──[0m [1;34mone-xattr_one-file[0m
|
||||
[38;5;244m│ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ └──[0m file-in-question
|
||||
[38;5;244m├──[0m [1;34mone-xattr_one-file_forbidden[0m
|
||||
[38;5;244m│ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m├──[0m [1;34mone-xattr_two-files[0m
|
||||
[38;5;244m│ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ ├──[0m that-file
|
||||
[38;5;244m│ └──[0m this-file
|
||||
[38;5;244m├──[0m [1;34mone-xattr_two-files_forbidden[0m
|
||||
[38;5;244m│ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m├──[0m [1;34mtwo-xattrs_empty[0m
|
||||
[38;5;244m│ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ └──[0m user.another_greeting (len 2)
|
||||
[38;5;244m├──[0m [1;34mtwo-xattrs_empty_forbidden[0m
|
||||
[38;5;244m│ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m├──[0m [1;34mtwo-xattrs_one-file[0m
|
||||
[38;5;244m│ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ ├──[0m user.another_greeting (len 2)
|
||||
[38;5;244m│ └──[0m file-in-question
|
||||
[38;5;244m├──[0m [1;34mtwo-xattrs_one-file_forbidden[0m
|
||||
[38;5;244m│ └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[38;5;244m├──[0m [1;34mtwo-xattrs_two-files[0m
|
||||
[38;5;244m│ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ ├──[0m user.another_greeting (len 2)
|
||||
[38;5;244m│ ├──[0m that-file
|
||||
[38;5;244m│ └──[0m this-file
|
||||
[38;5;244m└──[0m [1;34mtwo-xattrs_two-files_forbidden[0m
|
||||
[38;5;244m └──[0m [31m<Permission denied (os error 13)>[0m
|
||||
[36m/testcases/attributes/[1;34mfiles[0m
|
||||
[38;5;244m├──[0m no-xattrs
|
||||
[38;5;244m├──[0m no-xattrs_forbidden
|
||||
[38;5;244m├──[0m one-xattr
|
||||
[38;5;244m│ └──[0m user.greeting (len 5)
|
||||
[38;5;244m├──[0m one-xattr_forbidden
|
||||
[38;5;244m├──[0m two-xattrs
|
||||
[38;5;244m│ ├──[0m user.greeting (len 5)
|
||||
[38;5;244m│ └──[0m user.another_greeting (len 2)
|
||||
[38;5;244m└──[0m two-xattrs_forbidden
|
@ -1,4 +1,4 @@
|
||||
[4mPermissions[0m [4mSize[0m [4mUser[0m [4mDate Accessed[0m [4mName[0m
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 3 Mar 2003[0m plum
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m15 Jun 2006[0m pear
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m22 Jul 2009[0m peach
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m22 Dec 2009[0m peach
|
||||
|
4
xtests/dates_deifidom
Normal file
4
xtests/dates_deifidom
Normal file
@ -0,0 +1,4 @@
|
||||
[4mPermissions[0m [4mSize[0m [4mUser[0m [4mDate Modified[0m [4mName[0m
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m22 Dec 2009[0m plum
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m15 Jun 2006[0m peach
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 3 Mar 2003[0m pear
|
3
xtests/dates_fr
Normal file
3
xtests/dates_fr
Normal file
@ -0,0 +1,3 @@
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m15 juin 2006[0m peach
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 3 mars 2003[0m pear
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m22 déc. 2009[0m plum
|
@ -1,3 +1,3 @@
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2006-06-15 23:14:29.000000000 +0000[0m peach
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2003-03-03 00:00:00.000000000 +0000[0m pear
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2009-07-22 10:38:53.000000000 +0000[0m plum
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2009-12-22 10:38:53.000000000 +0000[0m plum
|
||||
|
@ -1,3 +1,3 @@
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m06-15 23:14[0m peach
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m03-03 00:00[0m pear
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m07-22 10:38[0m plum
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2006-06-15[0m peach
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2003-03-03[0m pear
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2009-12-22[0m plum
|
||||
|
3
xtests/dates_jp
Normal file
3
xtests/dates_jp
Normal file
@ -0,0 +1,3 @@
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m15 6月 2006[0m peach
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 3 3月 2003[0m pear
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m22 12月 2009[0m plum
|
@ -1,3 +1,3 @@
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2006-06-15 23:14[0m peach
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2003-03-03 00:00[0m pear
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2009-07-22 10:38[0m plum
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m2009-12-22 10:38[0m plum
|
||||
|
@ -1,4 +1,4 @@
|
||||
[4mPermissions[0m [4mSize[0m [4mUser[0m [4mDate Modified[0m [4mName[0m
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 3 Mar 2003[0m pear
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m15 Jun 2006[0m peach
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m22 Jul 2009[0m plum
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m22 Dec 2009[0m plum
|
||||
|
1
xtests/error_duplicate
Normal file
1
xtests/error_duplicate
Normal file
@ -0,0 +1 @@
|
||||
Flag -l conflicts with flag --long
|
1
xtests/error_long
Normal file
1
xtests/error_long
Normal file
@ -0,0 +1 @@
|
||||
Unknown argument --ternary
|
2
xtests/error_lt
Normal file
2
xtests/error_lt
Normal file
@ -0,0 +1,2 @@
|
||||
Flag -t needs a value (choices: modified, changed, accessed, created)
|
||||
To sort newest files last, try "--sort newest", or just "-snew"
|
2
xtests/error_ltr
Normal file
2
xtests/error_ltr
Normal file
@ -0,0 +1,2 @@
|
||||
Option --time (-t) has no "r" setting (choices: modified, changed, accessed, created)
|
||||
To sort oldest files last, try "--sort oldest", or just "-sold"
|
1
xtests/error_overvalued
Normal file
1
xtests/error_overvalued
Normal file
@ -0,0 +1 @@
|
||||
Flag --long cannot take a value
|
1
xtests/error_setting
Normal file
1
xtests/error_setting
Normal file
@ -0,0 +1 @@
|
||||
Option --time-style has no "24" setting (choices: default, long-iso, full-iso, iso)
|
1
xtests/error_short
Normal file
1
xtests/error_short
Normal file
@ -0,0 +1 @@
|
||||
Unknown argument -4
|
1
xtests/error_twice
Normal file
1
xtests/error_twice
Normal file
@ -0,0 +1 @@
|
||||
Flag -l was given twice
|
1
xtests/error_useless
Normal file
1
xtests/error_useless
Normal file
@ -0,0 +1 @@
|
||||
Option --binary (-b) is useless without option --long (-l)
|
1
xtests/error_value
Normal file
1
xtests/error_value
Normal file
@ -0,0 +1 @@
|
||||
Flag --time needs a value (choices: modified, changed, accessed, created)
|
@ -4,8 +4,11 @@
|
||||
compiled.coffee
|
||||
[38;5;137mcompiled.js[0m
|
||||
[38;5;137mcompiled.o[0m
|
||||
[31mcompressed.deb[0m
|
||||
[31mcompressed.tar.gz[0m
|
||||
[31mcompressed.tar.xz[0m
|
||||
[31mcompressed.tgz[0m
|
||||
[31mcompressed.txz[0m
|
||||
[31mCOMPRESSED.ZIP[0m
|
||||
[38;5;109mcrypto.asc[0m
|
||||
[38;5;109mcrypto.signature[0m
|
||||
|
6
xtests/file-names-exts-bw
Normal file
6
xtests/file-names-exts-bw
Normal file
@ -0,0 +1,6 @@
|
||||
#SAVEFILE# compressed.deb crypto.asc image.svg VIDEO.AVI
|
||||
backup~ compressed.tar.gz crypto.signature lossless.flac video.wmv
|
||||
compiled.class compressed.tar.xz document.pdf lossless.wav
|
||||
compiled.coffee compressed.tgz DOCUMENT.XLSX Makefile
|
||||
compiled.js compressed.txz file.tmp music.mp3
|
||||
compiled.o COMPRESSED.ZIP IMAGE.PNG MUSIC.OGG
|
@ -10,8 +10,11 @@
|
||||
compiled.coffee
|
||||
[38;5;137mcompiled.js[0m
|
||||
[38;5;137mcompiled.o[0m
|
||||
[31mcompressed.deb[0m
|
||||
[31mcompressed.tar.gz[0m
|
||||
[31mcompressed.tar.xz[0m
|
||||
[31mcompressed.tgz[0m
|
||||
[31mcompressed.txz[0m
|
||||
[38;5;109mcrypto.asc[0m
|
||||
[38;5;109mcrypto.signature[0m
|
||||
[38;5;105mdocument.pdf[0m
|
||||
|
@ -5,6 +5,7 @@
|
||||
[38;5;135mVIDEO.AVI[0m
|
||||
[38;5;137mcompiled.class[0m
|
||||
compiled.coffee
|
||||
[31mcompressed.deb[0m
|
||||
[38;5;93mlossless.flac[0m
|
||||
[31mcompressed.tar.gz[0m
|
||||
[38;5;137mcompiled.js[0m
|
||||
@ -17,7 +18,9 @@ compiled.coffee
|
||||
[38;5;133mimage.svg[0m
|
||||
[31mcompressed.tgz[0m
|
||||
[38;5;244mfile.tmp[0m
|
||||
[31mcompressed.txz[0m
|
||||
[38;5;93mlossless.wav[0m
|
||||
[38;5;135mvideo.wmv[0m
|
||||
[38;5;105mDOCUMENT.XLSX[0m
|
||||
[31mcompressed.tar.xz[0m
|
||||
[31mCOMPRESSED.ZIP[0m
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user