Merge branch 'modernise'

This commit is contained in:
Benjamin Sago 2020-10-13 01:28:55 +01:00
commit f42957fab8
64 changed files with 2202 additions and 2241 deletions

1
.rustfmt.toml Normal file
View File

@ -0,0 +1 @@
disable_all_formatting = true

450
Cargo.lock generated
View File

@ -1,538 +1,416 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "aho-corasick"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ansi_term"
version = "0.12.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "atty"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi",
]
[[package]]
name = "autocfg"
version = "0.1.5"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitflags"
version = "1.1.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "byteorder"
version = "1.3.2"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "cc"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cfg-if"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "datetime"
version = "0.5.0"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d"
dependencies = [
"iso8601 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
"locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.8 (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.56 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"jobserver",
]
[[package]]
name = "env_logger"
version = "0.6.2"
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "datetime"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0fcb4df22ae812fa2f6d5e3b577247584cc67fce06ad0779168d1dd41cbcce3"
dependencies = [
"atty 0.2.13 (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.7 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
"termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"iso8601",
"libc",
"locale",
"num-traits",
"pad",
"redox_syscall",
"winapi",
]
[[package]]
name = "exa"
version = "0.9.0"
dependencies = [
"ansi_term 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"datetime 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.6.2 (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.60 (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.7 (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.1 (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.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
"zoneinfo_compiled 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ansi_term",
"datetime",
"git2",
"glob",
"lazy_static",
"libc",
"locale",
"log",
"natord",
"num_cpus",
"number_prefix",
"scoped_threadpool",
"term_grid",
"term_size",
"unicode-width",
"users",
"zoneinfo_compiled",
]
[[package]]
name = "git2"
version = "0.9.1"
version = "0.13.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e094214efbc7fdbbdee952147e493b00e99a4e52817492277e98967ae918165"
dependencies = [
"bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.60 (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.7 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.48 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags",
"libc",
"libgit2-sys",
"log",
"openssl-sys",
"url",
]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "humantime"
version = "1.2.0"
name = "hermit-abi"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
dependencies = [
"quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc",
]
[[package]]
name = "idna"
version = "0.1.5"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
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.8 (registry+https://github.com/rust-lang/crates.io-index)",
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "iso8601"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43e86914a73535f3f541a765adea0a9fafcf53fa6adb73662c4988fd9233766f"
dependencies = [
"nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"nom",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
name = "jobserver"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
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)",
"libc",
]
[[package]]
name = "lazy_static"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.60"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743"
[[package]]
name = "libgit2-sys"
version = "0.8.1"
version = "0.12.13+1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069eea34f76ec15f2822ccf78fe0cdb8c9016764d0a12865278585a74dbdeae5"
dependencies = [
"cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.60 (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)",
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]]
name = "libz-sys"
version = "1.0.25"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655"
dependencies = [
"cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.60 (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.7 (registry+https://github.com/rust-lang/crates.io-index)",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "locale"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd"
dependencies = [
"libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
"libc",
]
[[package]]
name = "log"
version = "0.4.7"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
dependencies = [
"cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if",
]
[[package]]
name = "matches"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
[[package]]
name = "memchr"
version = "2.2.1"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
[[package]]
name = "natord"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
[[package]]
name = "nom"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
dependencies = [
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr",
"version_check",
]
[[package]]
name = "num-traits"
version = "0.2.8"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
dependencies = [
"autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.10.1"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
dependencies = [
"libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
"hermit-abi",
"libc",
]
[[package]]
name = "number_prefix"
version = "0.3.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "openssl-src"
version = "111.3.0+1.1.1c"
version = "111.11.0+1.1.1h"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380fe324132bea01f45239fadfec9343adb044615f29930d039bec1ae7b9fa5b"
dependencies = [
"cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.48"
version = "0.9.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
dependencies = [
"autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.60 (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.7 (registry+https://github.com/rust-lang/crates.io-index)",
"autocfg",
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "pad"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3"
dependencies = [
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width",
]
[[package]]
name = "percent-encoding"
version = "1.0.1"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pkg-config"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "quick-error"
version = "1.2.2"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
[[package]]
name = "redox_syscall"
version = "0.1.56"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "regex"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"aho-corasick 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.6.8 (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.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "regex-syntax"
version = "0.6.8"
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)",
]
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "scoped_threadpool"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "smallvec"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
[[package]]
name = "term_grid"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230d3e804faaed5a39b08319efb797783df2fd9671b39b7596490cb486d702cf"
dependencies = [
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width",
]
[[package]]
name = "term_size"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"libc",
"winapi",
]
[[package]]
name = "termcolor"
version = "1.0.5"
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 = "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"
name = "tinyvec"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117"
[[package]]
name = "unicode-bidi"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
dependencies = [
"matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
"matches",
]
[[package]]
name = "unicode-normalization"
version = "0.1.8"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977"
dependencies = [
"smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
"tinyvec",
]
[[package]]
name = "unicode-width"
version = "0.1.5"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]]
name = "url"
version = "1.7.2"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
dependencies = [
"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)",
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "users"
version = "0.10.0"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
dependencies = [
"libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
"libc",
"log",
]
[[package]]
name = "utf8-ranges"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "vcpkg"
version = "0.2.7"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
[[package]]
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi"
version = "0.3.8"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"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)",
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[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.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[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.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "zoneinfo_compiled"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7033eef97c288bfa49e3ebf958245a41016f1673a5317196efad03eb656a7648"
dependencies = [
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"datetime 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder",
"datetime",
]
[metadata]
"checksum aho-corasick 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "36b7aa1ccb7d7ea3f437cf025a2ab1c47cc6c1bc9fc84918ff449def12f5e282"
"checksum ansi_term 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eaa72766c3585a1f812a3387a7e2c6cab780f899c2f43ff6ea06c8d071fcbb36"
"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90"
"checksum autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "22130e92352b948e7e82a49cdb0aa94f2211761117f29e052dd397c1ac33542b"
"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd"
"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
"checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d"
"checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33"
"checksum datetime 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4ce78cadfc77450e21ff8bd11ca6f8669949b3b0bd8af92f9c1ee4fb453db7b"
"checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3"
"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.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "43e86914a73535f3f541a765adea0a9fafcf53fa6adb73662c4988fd9233766f"
"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.60 (registry+https://github.com/rust-lang/crates.io-index)" = "d44e80633f007889c7eff624b709ab43c92d708caad982295768a7b13ca3b5eb"
"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 log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c275b6ad54070ac2d665eef9197db647b32239c9d244bfb6f041a766d00da5b3"
"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
"checksum natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
"checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273"
"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.48 (registry+https://github.com/rust-lang/crates.io-index)" = "b5ba300217253bcc5dc68bed23d782affa45000193866e025329aa8a7a9f05b8"
"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.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
"checksum regex 1.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "d9d8297cc20bbb6184f8b45ff61c8ee6a9ac56c156cec8e38c3e5084773c44ad"
"checksum regex-syntax 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "9b01330cce219c1c6b2e209e5ed64ccd587ae5c67bed91c0b49eecf02ae40e21"
"checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
"checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7"
"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.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e"
"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.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.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
"checksum utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d50aa7650df78abf942826607c62468ce18d9019673d4a2ebe1865dbb96ffde"
"checksum vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "33dd455d0f96e90a75803cfeb7f948768c08d70a6de9a8d2362461935698bf95"
"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
"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.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7033eef97c288bfa49e3ebf958245a41016f1673a5317196efad03eb656a7648"

View File

@ -14,40 +14,33 @@ readme = "README.md"
categories = ["command-line-utilities"]
keywords = ["ls", "files", "command-line"]
license = "MIT"
exclude = ["/devtools/*", "/Makefile", "/Vagrantfile", "/screenshots.png"]
exclude = ["/devtools/*", "/Justfile", "/Vagrantfile", "/screenshots.png"]
[[bin]]
name = "exa"
path = "src/bin/main.rs"
doc = false
[lib]
name = "exa"
path = "src/exa.rs"
[dependencies]
ansi_term = "0.12.0"
ansi_term = "0.12"
datetime = "0.5"
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.10"
glob = "0.3"
lazy_static = "1.3"
libc = "0.2"
locale = "0.2"
log = "0.4"
natord = "1.0"
num_cpus = "1.10"
number_prefix = "0.4"
scoped_threadpool = "0.1"
term_grid = "0.1"
term_size = "0.3"
unicode-width = "0.1"
users = "0.11"
zoneinfo_compiled = "0.5"
[dependencies.git2]
version = "0.9.1"
version = "0.13"
optional = true
default-features = false

44
Justfile Normal file
View File

@ -0,0 +1,44 @@
all: build test
all-release: build-release test-release
# compiles the exa binary
@build:
cargo build
# compiles the exa binary (in release mode)
@build-release:
cargo build --release --verbose
# compiles the exa binary with every combination of feature flags
@build-features:
cargo hack build --feature-powerset
# runs unit tests
@test:
cargo test --all -- --quiet
# runs unit tests (in release mode)
@test-release:
cargo test --release --all --verbose
# runs unit tests with every combination of feature flags
@test-features:
cargo hack test --feature-powerset -- --quiet
# lints the code
@clippy:
touch src/main.rs
cargo clippy
# updates dependency versions, and checks for outdated ones
@update:
cargo update
cargo outdated
# prints versions of the necessary build tools
@versions:
rustc --version
cargo --version

View File

@ -1,86 +0,0 @@
DESTDIR =
PREFIX = /usr/local
override define compdir
ifndef $(1)
$(1) := $$(or $$(shell pkg-config --variable=completionsdir $(2) 2>/dev/null),$(3))
endif
endef
$(eval $(call compdir,BASHDIR,bash-completion,$(PREFIX)/etc/bash_completion.d))
$(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 $(CARGO_OPTS)
install: install-exa install-man
install-exa: target/release/exa
install -m755 -- target/release/exa "$(DESTDIR)$(PREFIX)/bin/"
install-man:
install -dm755 -- "$(DESTDIR)$(PREFIX)/bin/" "$(DESTDIR)$(PREFIX)/share/man/man1/"
install -m644 -- contrib/man/exa.1 "$(DESTDIR)$(PREFIX)/share/man/man1/"
install-bash-completions:
install -m644 -- contrib/completions.bash "$(DESTDIR)$(BASHDIR)/exa"
install-zsh-completions:
install -m644 -- contrib/completions.zsh "$(DESTDIR)$(ZSHDIR)/_exa"
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"
-rm -f -- "$(DESTDIR)$(BASHDIR)/exa"
-rm -f -- "$(DESTDIR)$(ZSHDIR)/_exa"
-rm -f -- "$(DESTDIR)$(FISHDIR)/exa.fish"
clean:
cargo clean
preview-man:
man contrib/man/exa.1
help:
@echo 'Available make targets:'
@echo ' all - build exa (default)'
@echo ' build - build exa'
@echo ' clean - run `cargo clean`'
@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'
@echo
@echo ' install-bash-completions - install bash completions into $$BASHDIR'
@echo ' install-zsh-completions - install zsh completions into $$ZSHDIR'
@echo ' install-fish-completions - install fish completions into $$FISHDIR'
@echo
@echo 'Variables:'
@echo ' DESTDIR - A path that'\''s prepended to installation paths (default: "")'
@echo ' PREFIX - The installation prefix for everything except zsh completions (default: /usr/local)'
@echo ' BASHDIR - The directory to install bash completions in (default: $$PREFIX/etc/bash_completion.d)'
@echo ' ZSHDIR - The directory to install zsh completions in (default: /usr/share/zsh/vendor-completions)'
@echo ' FISHDIR - The directory to install fish completions in (default: $$PREFIX/share/fish/vendor_completions.d)'
@echo ' FEATURES - The cargo feature flags to use. Set to an empty string to disable git support'
.PHONY: all build target/release/exa install-exa install-man preview-man \
install-bash-completions install-zsh-completions install-fish-completions \
clean uninstall help

67
Vagrantfile vendored
View File

@ -4,19 +4,18 @@ Vagrant.configure(2) do |config|
# We use Ubuntu instead of Debian because the image comes with two-way
# shared folder support by default.
UBUNTU = 'bento/ubuntu-16.04'
UBUNTU = 'hashicorp/bionic64'
# The main VM is the one used for development and testing.
config.vm.define(:exa, primary: true) do |config|
config.vm.define(:exa) do |config|
config.vm.provider :virtualbox do |v|
v.name = 'exa'
v.memory = 2048
v.cpus = 2
v.cpus = `nproc`.chomp.to_i
end
config.vm.provider :vmware_desktop do |v|
v.vmx['memsize'] = '2048'
v.vmx['numvcpus'] = '2'
v.vmx['numvcpus'] = `nproc`.chomp
end
config.vm.box = UBUNTU
@ -56,6 +55,19 @@ Vagrant.configure(2) do |config|
else
set -xe
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
cargo install cargo-hack
fi
EOF
# Install Just, the command runner.
config.vm.provision :shell, privileged: true, inline: <<-EOF
if hash just &>/dev/null; then
echo "just is already installed"
else
wget "https://github.com/casey/just/releases/download/v0.8.0/just-v0.8.0-x86_64-unknown-linux-musl.tar.gz"
tar -xf "just-v0.8.0-x86_64-unknown-linux-musl.tar.gz"
cp just /usr/local/bin
fi
EOF
@ -80,7 +92,7 @@ Vagrant.configure(2) do |config|
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
echo -e "#!/bin/sh\ncargo test --manifest-path /vagrant/Cargo.toml \\$@ -- --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
@ -562,47 +574,4 @@ Vagrant.configure(2) do |config|
EOF
end
end
# Remember that problem that exa had where the binary wasnt 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

View File

@ -11,8 +11,9 @@
/// - https://crates.io/crates/vergen
extern crate datetime;
use std::io::Result as IOResult;
use std::env;
use std::io;
fn git_hash() -> String {
use std::process::Command;
@ -45,14 +46,15 @@ fn build_date() -> String {
format!("{}", now.date().iso())
}
fn write_statics() -> IOResult<()> {
fn write_statics() -> io::Result<()> {
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
let ver = if is_development_version() {
format!("exa v{} ({} built on {})", cargo_version(), git_hash(), build_date())
} else {
}
else {
format!("exa v{}", cargo_version())
};

View File

@ -1,5 +1,5 @@
## exa development tools
## exa development tools
These scripts deal with things like packaging release-worthy versions of exa and making sure the published versions actually work.
These scripts deal with things like packaging release-worthy versions of exa.
They are **not general-purpose scripts** that youre able to run from your main computer! Theyre intended to be run from the Vagrant machines — they have commands such as package-exa or check-release that execute them instead.
They are **not general-purpose scripts** that youre able to run from your main computer! Theyre intended to be run from the Vagrant machine.

View File

@ -12,7 +12,7 @@ bash /vagrant/devtools/dev-versions.sh
# The Cool Prompt tells you whether youre 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 debug_mode() { [ "$EXA_DEBUG" == "trace" ] && echo -n "trace-"; [ -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 "; }
@ -22,10 +22,14 @@ export PS1="\[\e[1;36m\]\h \[\e[32m\]\w \[\e[31m\]\`nonzero_return\`\[\e[35m\]\`
# The debug function lets you switch debug mode on and off.
# Turn it on if you need to see exas 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; }
case "$1" in
""|"on") export EXA_DEBUG=1 ;;
"off") export EXA_DEBUG= ;;
"trace") export EXA_DEBUG=trace ;;
"status") [ -n "$EXA_DEBUG" ] && echo "debug on" || echo "debug off" ;;
*) echo "Usage: debug on|off|trace|status"; return 1 ;;
esac;
}
# The strict function lets you switch strict mode on and off.
# Turn it on if youd like exas command-line arguments checked.
@ -33,7 +37,9 @@ 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; }
*) echo "Usage: strict on|off"; return 1 ;;
esac;
}
# The colors function sets or unsets the LS_COLORS and EXA_COLORS
# environment variables. Theres also a hacker theme which turns everything
@ -53,4 +59,6 @@ function 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; }
*) echo "Usage: ls-colors ls|hacker|off"; return 1 ;;
esac;
}

View File

@ -1,51 +0,0 @@
# 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 its 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"

View File

@ -1,15 +0,0 @@
# This script generates the MD5SUMS and SHA1SUMS files.
# Youll 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

View File

@ -1,12 +0,0 @@
# 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
"

View File

@ -1,85 +0,0 @@
extern crate exa;
use exa::Exa;
use std::ffi::OsString;
use std::env::{args_os, var_os};
use std::io::{stdout, stderr, Write, ErrorKind};
use std::process::exit;
fn main() {
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),
Err(e) => {
match e.kind() {
ErrorKind::BrokenPipe => exit(exits::SUCCESS),
_ => {
eprintln!("{}", e);
exit(exits::RUNTIME_ERROR);
},
};
}
};
},
Err(ref e) if e.is_error() => {
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) => {
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 cant be done in exas 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 {
use libc::{self, c_int};
pub const SUCCESS: c_int = libc::EXIT_SUCCESS;
pub const RUNTIME_ERROR: c_int = libc::EXIT_FAILURE;
pub const OPTIONS_ERROR: c_int = 3 as c_int;
}

View File

@ -1,227 +0,0 @@
#![warn(trivial_casts, trivial_numeric_casts)]
#![warn(unused_results)]
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 log::debug;
use crate::fs::{Dir, File};
use crate::fs::filter::GitIgnore;
use crate::fs::feature::git::GitCache;
use crate::options::{Options, Vars};
pub use crate::options::vars;
pub use crate::options::Misfire;
use crate::output::{escape, lines, grid, grid_details, details, View, Mode};
mod fs;
mod info;
mod options;
mod output;
mod style;
/// The main program wrapper.
pub struct Exa<'args, 'w, W: Write + 'w> {
/// List of command-line options, having been successfully parsed.
pub options: Options,
/// The output handle that we write to. When running the program normally,
/// this will be `std::io::Stdout`, but it can accept any struct thats
/// `Write` so we can write into, say, a vector for testing.
pub writer: &'w mut W,
/// List of the free command-line arguments that should correspond to file
/// names (anything that isnt an option).
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>,
}
/// 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 theyre 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
}
}
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 wont see it.
if args.is_empty() {
args = vec![ OsStr::new(".") ];
}
let git = git_options(&options, &args);
Exa { options, writer, args, git }
})
}
pub fn run(&mut self) -> IOResult<i32> {
let mut files = Vec::new();
let mut dirs = Vec::new();
let mut exit_status = 0;
for file_path in &self.args {
match File::from_args(PathBuf::from(file_path), None, None) {
Err(e) => {
exit_status = 2;
writeln!(stderr(), "{:?}: {}", file_path, e)?;
},
Ok(f) => {
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_path, e)?,
}
}
else {
files.push(f);
}
},
}
}
// We want to print a directorys name before we list it, *except* in
// the case where its the only directory, *except* if there are any
// files to print as well. (Its a double negative)
let no_files = files.is_empty();
let is_only_dir = dirs.len() == 1 && no_files;
self.options.filter.filter_argument_files(&mut files);
self.print_files(None, files)?;
self.print_dirs(dirs, no_files, is_only_dir, exit_status)
}
fn print_dirs(&mut self, dir_files: Vec<Dir>, mut first: bool, is_only_dir: bool, exit_status: i32) -> IOResult<i32> {
for dir in dir_files {
// Put a gap between directories, or between the list of files and
// the first directory.
if first {
first = false;
}
else {
writeln!(self.writer)?;
}
if !is_only_dir {
let mut bits = Vec::new();
escape(dir.path.display().to_string(), &mut bits, Style::default(), Style::default());
writeln!(self.writer, "{}:", ANSIStrings(&bits))?;
}
let mut children = Vec::new();
let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
for file in dir.files(self.options.filter.dot_filter, self.git.as_ref(), git_ignore) {
match file {
Ok(file) => children.push(file),
Err((path, e)) => writeln!(stderr(), "[{}: {}]", path.display(), e)?,
}
};
self.options.filter.filter_child_files(&mut children);
self.options.filter.sort_files(&mut children);
if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1;
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() && !f.is_all_all) {
match child_dir.to_dir() {
Ok(d) => child_dirs.push(d),
Err(e) => writeln!(stderr(), "{}: {}", child_dir.path.display(), e)?,
}
}
self.print_files(Some(&dir), children)?;
match self.print_dirs(child_dirs, false, false, exit_status) {
Ok(_) => (),
Err(e) => return Err(e),
}
continue;
}
}
self.print_files(Some(&dir), children)?;
}
Ok(exit_status)
}
/// Prints the list of files using whichever view is selected.
/// For various annoying logistical reasons, each one handles
/// printing differently...
fn print_files(&mut self, dir: Option<&Dir>, files: Vec<File>) -> IOResult<()> {
if !files.is_empty() {
let View { ref mode, ref colours, ref style } = self.options.view;
match *mode {
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 git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
let r = details::Render { dir, files, colours, style, opts, filter, recurse, git_ignoring };
r.render(self.git.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 git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
let r = grid_details::Render { dir, files, colours, style, grid, details, filter, row_threshold, git_ignoring };
r.render(self.git.as_ref(), self.writer)
}
}
}
else {
Ok(())
}
}
}

View File

@ -1,16 +1,16 @@
use crate::fs::feature::git::GitCache;
use crate::fs::fields::GitStatus;
use std::io::{self, Result as IOResult};
use std::io;
use std::fs;
use std::path::{Path, PathBuf};
use std::slice::Iter as SliceIter;
use log::info;
use log::*;
use crate::fs::File;
/// A **Dir** provides a cached list of the file paths in a directory that's
/// A **Dir** provides a cached list of the file paths in a directory thats
/// being listed.
///
/// This object gets passed to the Files themselves, in order for them to
@ -35,14 +35,14 @@ impl Dir {
/// The `read_dir` iterator doesnt actually yield the `.` and `..`
/// entries, so if the user wants to see them, well have to add them
/// ourselves after the files have been read.
pub fn read_dir(path: PathBuf) -> IOResult<Dir> {
pub fn read_dir(path: PathBuf) -> io::Result<Self> {
info!("Reading directory {:?}", &path);
let contents = fs::read_dir(&path)?
.map(|result| result.map(|entry| entry.path()))
.collect::<Result<_,_>>()?;
.map(|result| result.map(|entry| entry.path()))
.collect::<Result<_, _>>()?;
Ok(Dir { contents, path })
Ok(Self { contents, path })
}
/// Produce an iterator of IO results of trying to read all the files in
@ -53,7 +53,8 @@ impl Dir {
dir: self,
dotfiles: dots.shows_dotfiles(),
dots: dots.dots(),
git, git_ignoring,
git,
git_ignoring,
}
}
@ -106,7 +107,9 @@ impl<'dir, 'ig> Files<'dir, 'ig> {
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;
}
if self.git_ignoring {
let git_status = self.git.map(|g| g.get(path, false)).unwrap_or_default();
@ -139,7 +142,6 @@ enum DotsNext {
Files,
}
impl<'dir, 'ig> Iterator for Files<'dir, 'ig> {
type Item = Result<File<'dir>, (PathBuf, io::Error)>;
@ -149,22 +151,24 @@ impl<'dir, 'ig> Iterator for Files<'dir, 'ig> {
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()
},
}
}
}
}
/// Usually files in Unix use a leading dot to be hidden or visible, but two
/// entries in particular are "extra-hidden": `.` and `..`, which only become
/// entries in particular are “extra-hidden”: `.` and `..`, which only become
/// visible after an extra `-a` option.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum DotFilter {
@ -180,8 +184,8 @@ pub enum DotFilter {
}
impl Default for DotFilter {
fn default() -> DotFilter {
DotFilter::JustFiles
fn default() -> Self {
Self::JustFiles
}
}
@ -190,18 +194,18 @@ impl DotFilter {
/// Whether this filter should show dotfiles in a listing.
fn shows_dotfiles(self) -> bool {
match self {
DotFilter::JustFiles => false,
DotFilter::Dotfiles => true,
DotFilter::DotfilesAndDots => true,
Self::JustFiles => false,
Self::Dotfiles => true,
Self::DotfilesAndDots => true,
}
}
/// Whether this filter should add dot directories to a listing.
fn dots(self) -> DotsNext {
match self {
DotFilter::JustFiles => DotsNext::Files,
DotFilter::Dotfiles => DotsNext::Files,
DotFilter::DotfilesAndDots => DotsNext::Dot,
Self::JustFiles => DotsNext::Files,
Self::Dotfiles => DotsNext::Files,
Self::DotfilesAndDots => DotsNext::Dot,
}
}
}

View File

@ -39,19 +39,19 @@ pub enum DirAction {
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,
pub fn recurse_options(self) -> Option<RecurseOptions> {
match self {
Self::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,
pub fn treat_dirs_as_files(self) -> bool {
match self {
Self::AsFile => true,
Self::Recurse(o) => o.tree,
_ => false,
}
}
}
@ -73,10 +73,10 @@ pub struct RecurseOptions {
impl RecurseOptions {
/// Returns whether a directory of the given depth would be too deep.
pub fn is_too_deep(&self, depth: usize) -> bool {
pub fn is_too_deep(self, depth: usize) -> bool {
match self.max_depth {
None => false,
Some(d) => d <= depth
None => false,
Some(d) => d <= depth
}
}
}

View File

@ -3,7 +3,7 @@
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use log::{debug, error, info, warn};
use log::*;
use crate::fs::fields as f;
@ -36,9 +36,11 @@ impl GitCache {
use std::iter::FromIterator;
impl FromIterator<PathBuf> for GitCache {
fn from_iter<I: IntoIterator<Item=PathBuf>>(iter: I) -> Self {
fn from_iter<I>(iter: I) -> Self
where I: IntoIterator<Item=PathBuf>
{
let iter = iter.into_iter();
let mut git = GitCache {
let mut git = Self {
repos: Vec::with_capacity(iter.size_hint().0),
misses: Vec::new(),
};
@ -61,8 +63,10 @@ impl FromIterator<PathBuf> for GitCache {
debug!("Discovered new Git repo");
git.repos.push(r);
},
Err(miss) => git.misses.push(miss),
}
Err(miss) => {
git.misses.push(miss)
}
}
}
}
@ -72,8 +76,6 @@ impl FromIterator<PathBuf> for GitCache {
}
/// A **Git repository** is one weve discovered somewhere on the filesystem.
pub struct GitRepo {
@ -99,7 +101,9 @@ pub struct GitRepo {
enum GitContents {
/// All the interesting Git stuff goes through this.
Before { repo: git2::Repository },
Before {
repo: git2::Repository,
},
/// Temporary value used in `repo_to_statuses` so we can move the
/// repository out of the `Before` variant.
@ -107,7 +111,9 @@ enum GitContents {
/// The data weve extracted from the repository, but only after weve
/// actually done so.
After { statuses: Git }
After {
statuses: Git,
},
}
impl GitRepo {
@ -116,27 +122,26 @@ impl GitRepo {
/// 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
/// Git statuses is only done once, and gets cached so we dont 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
/// 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 {
if let GitContents::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 repo = replace(&mut *contents, GitContents::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 });
let _processing = replace(&mut *contents, GitContents::After { statuses });
result
}
@ -152,7 +157,7 @@ impl GitRepo {
/// 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> {
fn discover(path: PathBuf) -> Result<Self, PathBuf> {
info!("Searching for Git repository above {:?}", path);
let repo = match git2::Repository::discover(&path) {
Ok(r) => r,
@ -162,11 +167,12 @@ impl GitRepo {
}
};
match repo.workdir().map(|wd| wd.to_path_buf()) {
match repo.workdir() {
Some(workdir) => {
let workdir = workdir.to_path_buf();
let contents = Mutex::new(GitContents::Before { repo });
Ok(GitRepo { contents, workdir, original_path: path, extra_paths: Vec::new() })
},
Ok(Self { contents, workdir, original_path: path, extra_paths: Vec::new() })
}
None => {
warn!("Repository has no workdir?");
Err(path)
@ -181,7 +187,7 @@ impl GitContents {
/// (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 {
if let Self::Before { repo } = self {
repo
}
else {
@ -205,8 +211,10 @@ fn repo_to_statuses(repo: &git2::Repository, workdir: &Path) -> Git {
let elem = (path, e.status());
statuses.push(elem);
}
},
Err(e) => error!("Error looking up Git statuses: {:?}", e),
}
Err(e) => {
error!("Error looking up Git statuses: {:?}", e)
}
}
Git { statuses }
@ -217,7 +225,7 @@ fn repo_to_statuses(repo: &git2::Repository, workdir: &Path) -> Git {
// 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
// Even inserting another logging line immediately afterwards doesnt make it
// look any faster.
@ -239,6 +247,7 @@ impl Git {
/// 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) })
@ -250,25 +259,31 @@ impl Git {
/// directories, which dont 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(&path))
.fold(git2::Status::empty(), |a, b| a | b.1);
f::Git { staged: index_status(s), unstaged: working_tree_status(s) }
let s = self.statuses.iter()
.filter(|p| p.0.starts_with(&path))
.fold(git2::Status::empty(), |a, b| a | b.1);
let staged = index_status(s);
let unstaged = working_tree_status(s);
f::Git { staged, unstaged }
}
}
/// Converts a path to an absolute path based on the current directory.
/// Paths need to be absolute for them to be compared properly, otherwise
/// youd ask a repo about “./README.md” but it only knows about
/// “/vagrant/README.md”, prefixed by the workdir.
fn reorient(path: &Path) -> PathBuf {
use std::env::current_dir;
// Im not 100% on this func tbh
// TODO: Im not 100% on this func tbh
let path = match current_dir() {
Err(_) => Path::new(".").join(&path),
Ok(dir) => dir.join(&path),
Err(_) => Path::new(".").join(&path),
Ok(dir) => dir.join(&path),
};
path.canonicalize().unwrap_or(path)
}

View File

@ -1,8 +1,9 @@
pub mod xattr;
#[cfg(feature="git")] pub mod git;
#[cfg(feature = "git")]
pub mod git;
#[cfg(not(feature="git"))]
#[cfg(not(feature = "git"))]
pub mod git {
use std::iter::FromIterator;
use std::path::{Path, PathBuf};
@ -13,8 +14,10 @@ pub mod git {
pub struct GitCache;
impl FromIterator<PathBuf> for GitCache {
fn from_iter<I: IntoIterator<Item=PathBuf>>(_iter: I) -> Self {
GitCache
fn from_iter<I>(_iter: I) -> Self
where I: IntoIterator<Item=PathBuf>
{
Self
}
}

View File

@ -1,4 +1,5 @@
//! Extended attribute support for Darwin and Linux systems.
#![allow(trivial_casts)] // for ARM
extern crate libc;
@ -6,7 +7,11 @@ use std::cmp::Ordering;
use std::io;
use std::path::Path;
pub const ENABLED: bool = cfg!(feature="git") && cfg!(any(target_os="macos", target_os="linux"));
pub const ENABLED: bool =
cfg!(feature="git") &&
cfg!(any(target_os = "macos", target_os = "linux"));
pub trait FileAttributes {
fn attributes(&self) -> io::Result<Vec<Attribute>>;
@ -27,20 +32,21 @@ impl FileAttributes for Path {
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
impl FileAttributes for Path {
fn attributes(&self) -> io::Result<Vec<Attribute>> {
Ok(vec![])
Ok(Vec::new())
}
fn symlink_attributes(&self) -> io::Result<Vec<Attribute>> {
Ok(vec![])
Ok(Vec::new())
}
}
/// Attributes which can be passed to `Attribute::list_with_flags`
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[derive(Copy, Clone)]
pub enum FollowSymlinks {
Yes,
No
No,
}
/// Extended attribute
@ -50,29 +56,32 @@ pub struct Attribute {
pub size: usize,
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attribute>> {
use std::ffi::CString;
let c_path = match path.to_str().and_then(|s| { CString::new(s).ok() }) {
let c_path = match path.to_str().and_then(|s| CString::new(s).ok()) {
Some(cstring) => cstring,
None => return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?")),
None => {
return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?"));
}
};
let bufsize = lister.listxattr_first(&c_path);
match bufsize.cmp(&0) {
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Equal => return Ok(Vec::new()),
Ordering::Greater => {},
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Equal => return Ok(Vec::new()),
Ordering::Greater => {},
}
let mut buf = vec![0u8; bufsize as usize];
let mut buf = vec![0_u8; bufsize as usize];
let err = lister.listxattr_second(&c_path, &mut buf, bufsize);
match err.cmp(&0) {
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Equal => return Ok(Vec::new()),
Ordering::Greater => {},
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Equal => return Ok(Vec::new()),
Ordering::Greater => {},
}
let mut names = Vec::new();
@ -91,33 +100,40 @@ pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attrib
if size > 0 {
names.push(Attribute {
name: lister.translate_attribute_name(&buf[start..end]),
size: size as usize
size: size as usize,
});
}
start = c_end;
}
}
Ok(names)
}
#[cfg(target_os = "macos")]
mod lister {
use std::ffi::CString;
use libc::{c_int, size_t, ssize_t, c_char, c_void, uint32_t};
use super::FollowSymlinks;
use libc::{c_int, size_t, ssize_t, c_char, c_void};
use std::ffi::CString;
use std::ptr;
extern "C" {
fn listxattr(
path: *const c_char, namebuf: *mut c_char,
size: size_t, options: c_int
path: *const c_char,
namebuf: *mut c_char,
size: size_t,
options: c_int,
) -> ssize_t;
fn getxattr(
path: *const c_char, name: *const c_char,
value: *mut c_void, size: size_t, position: uint32_t,
options: c_int
path: *const c_char,
name: *const c_char,
value: *mut c_void,
size: size_t,
position: u32,
options: c_int,
) -> ssize_t;
}
@ -126,26 +142,27 @@ mod lister {
}
impl Lister {
pub fn new(do_follow: FollowSymlinks) -> Lister {
pub fn new(do_follow: FollowSymlinks) -> Self {
let c_flags: c_int = match do_follow {
FollowSymlinks::Yes => 0x0001,
FollowSymlinks::No => 0x0000,
FollowSymlinks::Yes => 0x0001,
FollowSymlinks::No => 0x0000,
};
Lister { c_flags }
Self { c_flags }
}
pub fn translate_attribute_name(&self, input: &[u8]) -> String {
use std::str::from_utf8_unchecked;
unsafe {
from_utf8_unchecked(input).into()
}
unsafe { std::str::from_utf8_unchecked(input).into() }
}
pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
unsafe {
listxattr(c_path.as_ptr(), ptr::null_mut(), 0, self.c_flags)
listxattr(
c_path.as_ptr(),
ptr::null_mut(),
0,
self.c_flags,
)
}
}
@ -154,7 +171,8 @@ mod lister {
listxattr(
c_path.as_ptr(),
buf.as_mut_ptr() as *mut c_char,
bufsize as size_t, self.c_flags
bufsize as size_t,
self.c_flags,
)
}
}
@ -164,13 +182,17 @@ mod lister {
getxattr(
c_path.as_ptr(),
buf.as_ptr() as *const c_char,
ptr::null_mut(), 0, 0, self.c_flags
ptr::null_mut(),
0,
0,
self.c_flags,
)
}
}
}
}
#[cfg(target_os = "linux")]
mod lister {
use std::ffi::CString;
@ -180,21 +202,29 @@ mod lister {
extern "C" {
fn listxattr(
path: *const c_char, list: *mut c_char, size: size_t
path: *const c_char,
list: *mut c_char,
size: size_t,
) -> ssize_t;
fn llistxattr(
path: *const c_char, list: *mut c_char, size: size_t
path: *const c_char,
list: *mut c_char,
size: size_t,
) -> ssize_t;
fn getxattr(
path: *const c_char, name: *const c_char,
value: *mut c_void, size: size_t
path: *const c_char,
name: *const c_char,
value: *mut c_void,
size: size_t,
) -> ssize_t;
fn lgetxattr(
path: *const c_char, name: *const c_char,
value: *mut c_void, size: size_t
path: *const c_char,
name: *const c_char,
value: *mut c_void,
size: size_t,
) -> ssize_t;
}
@ -213,41 +243,46 @@ mod lister {
pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
let listxattr = match self.follow_symlinks {
FollowSymlinks::Yes => listxattr,
FollowSymlinks::No => llistxattr,
FollowSymlinks::Yes => listxattr,
FollowSymlinks::No => llistxattr,
};
unsafe {
listxattr(c_path.as_ptr() as *const _, ptr::null_mut(), 0)
listxattr(
c_path.as_ptr() as *const _,
ptr::null_mut(),
0,
)
}
}
pub fn listxattr_second(&self, c_path: &CString, buf: &mut Vec<u8>, bufsize: ssize_t) -> ssize_t {
let listxattr = match self.follow_symlinks {
FollowSymlinks::Yes => listxattr,
FollowSymlinks::No => llistxattr,
FollowSymlinks::Yes => listxattr,
FollowSymlinks::No => llistxattr,
};
unsafe {
listxattr(
c_path.as_ptr() as *const _,
buf.as_mut_ptr() as *mut c_char,
bufsize as size_t
bufsize as size_t,
)
}
}
pub fn getxattr(&self, c_path: &CString, buf: &[u8]) -> ssize_t {
let getxattr = match self.follow_symlinks {
FollowSymlinks::Yes => getxattr,
FollowSymlinks::No => lgetxattr,
FollowSymlinks::Yes => getxattr,
FollowSymlinks::No => lgetxattr,
};
unsafe {
getxattr(
c_path.as_ptr() as *const _,
buf.as_ptr() as *const c_char,
ptr::null_mut(), 0
ptr::null_mut(),
0,
)
}
}

View File

@ -1,4 +1,3 @@
//! Wrapper types for the values returned from `File`s.
//!
//! The methods of `File` that return information about the entry on the
@ -43,22 +42,27 @@ pub type uid_t = u32;
/// regular file. (See the `filetype` module for those checks.)
///
/// Its ordering is used when sorting by type.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
pub enum Type {
Directory, File, Link, Pipe, Socket, CharDevice, BlockDevice, Special,
Directory,
File,
Link,
Pipe,
Socket,
CharDevice,
BlockDevice,
Special,
}
impl Type {
pub fn is_regular_file(&self) -> bool {
match *self {
Type::File => true,
_ => false,
}
pub fn is_regular_file(self) -> bool {
matches!(self, Self::File)
}
}
/// The files Unix permission bitfield, with one entry per bit.
#[derive(Copy, Clone)]
pub struct Permissions {
pub user_read: bool,
pub user_write: bool,
@ -80,6 +84,7 @@ pub struct Permissions {
/// The three pieces of information that are displayed as a single column in
/// the details view. These values are fused together to make the output a
/// little more compressed.
#[derive(Copy, Clone)]
pub struct PermissionsPlus {
pub file_type: Type,
pub permissions: Permissions,
@ -88,6 +93,7 @@ pub struct PermissionsPlus {
/// The permissions encoded as octal values
#[derive(Copy, Clone)]
pub struct OctalPermissions {
pub permissions: Permissions,
}
@ -98,6 +104,7 @@ pub struct OctalPermissions {
/// multiple directories. However, its rare (but occasionally useful!) for a
/// regular file to have a link count greater than 1, so we highlight the
/// block count specifically for this case.
#[derive(Copy, Clone)]
pub struct Links {
/// The actual link count.
@ -111,10 +118,12 @@ pub struct Links {
/// A files inode. Every directory entry on a Unix filesystem has an inode,
/// including directories and links, so this is applicable to everything exa
/// can deal with.
#[derive(Copy, Clone)]
pub struct Inode(pub ino_t);
/// The number of blocks that a file takes up on the filesystem, if any.
#[derive(Copy, Clone)]
pub enum Blocks {
/// This file has the given number of blocks.
@ -127,14 +136,17 @@ pub enum Blocks {
/// The ID of the user that owns a file. This will only ever be a number;
/// looking up the username is done in the `display` module.
#[derive(Copy, Clone)]
pub struct User(pub uid_t);
/// The ID of the group that a file belongs to.
#[derive(Copy, Clone)]
pub struct Group(pub gid_t);
/// A files size, in bytes. This is usually formatted by the `number_prefix`
/// crate into something human-readable.
#[derive(Copy, Clone)]
pub enum Size {
/// This file has a defined size.
@ -146,7 +158,7 @@ pub enum Size {
/// have a file size. For example, a directory will just contain a list of
/// its files as its “contents” and will be specially flagged as being a
/// directory, rather than a file. However, seeing the “file size” of this
/// data is rarely useful -- I cant think of a time when Ive seen it and
/// data is rarely useful I cant think of a time when Ive seen it and
/// learnt something. So we discard it and just output “-” instead.
///
/// See this answer for more: http://unix.stackexchange.com/a/68266
@ -163,8 +175,9 @@ pub enum Size {
/// The major and minor device IDs that gets displayed for device files.
///
/// You can see what these device numbers mean:
/// - http://www.lanana.org/docs/device-list/
/// - http://www.lanana.org/docs/device-list/devices-2.6+.txt
/// - <http://www.lanana.org/docs/device-list/>
/// - <http://www.lanana.org/docs/device-list/devices-2.6+.txt>
#[derive(Copy, Clone)]
pub struct DeviceIDs {
pub major: u8,
pub minor: u8,
@ -182,7 +195,7 @@ pub struct Time {
/// A files status in a Git repository. Whether a file is in a repository or
/// not is handled by the Git module, rather than having a “null” variant in
/// this enum.
#[derive(PartialEq)]
#[derive(PartialEq, Copy, Clone)]
pub enum GitStatus {
/// This file hasnt changed since the last commit.
@ -207,23 +220,27 @@ pub enum GitStatus {
/// A file thats ignored (that matches a line in .gitignore)
Ignored,
/// A file that's updated but unmerged.
/// A file thats updated but unmerged.
Conflicted,
}
/// A files complete Git status. Its possible to make changes to a file, add
/// it to the staging area, then make *more* changes, so we need to list each
/// files status for both of these.
#[derive(Copy, Clone)]
pub struct Git {
pub staged: GitStatus,
pub unstaged: GitStatus,
}
use std::default::Default;
impl Default for Git {
/// Create a Git status for a file with nothing done to it.
fn default() -> Git {
Git { staged: GitStatus::NotModified, unstaged: GitStatus::NotModified }
fn default() -> Self {
Self {
staged: GitStatus::NotModified,
unstaged: GitStatus::NotModified,
}
}
}

View File

@ -1,17 +1,17 @@
//! Files, and methods and fields to access their metadata.
use std::io::Error as IOError;
use std::io::Result as IOResult;
use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt};
use std::io;
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use log::{debug, error};
use log::*;
use crate::fs::dir::Dir;
use crate::fs::fields as f;
/// A **File** is a wrapper around one of Rust's Path objects, along with
/// A **File** is a wrapper around one of Rusts `PathBuf` values, along with
/// associated data about the file.
///
/// Each file is definitely going to have its filename displayed at least
@ -44,7 +44,7 @@ pub struct File<'dir> {
///
/// 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's better to just cache it.
/// its better to just cache it.
pub metadata: std::fs::Metadata,
/// A reference to the directory that contains this file, if any.
@ -60,13 +60,13 @@ pub struct File<'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
/// directorys 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 from_args<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) -> io::Result<File<'dir>>
where PD: Into<Option<&'dir Dir>>,
FN: Into<Option<String>>
{
@ -81,25 +81,27 @@ impl<'dir> File<'dir> {
Ok(File { path, parent_dir, metadata, ext, name, is_all_all })
}
pub fn new_aa_current(parent_dir: &'dir Dir) -> IOResult<File<'dir>> {
pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
let path = parent_dir.path.to_path_buf();
let ext = File::ext(&path);
debug!("Statting file {:?}", &path);
let metadata = std::fs::symlink_metadata(&path)?;
let is_all_all = true;
let parent_dir = Some(parent_dir);
Ok(File { path, parent_dir: Some(parent_dir), metadata, ext, name: ".".to_string(), is_all_all })
Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all })
}
pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> IOResult<File<'dir>> {
pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
let ext = File::ext(&path);
debug!("Statting file {:?}", &path);
let metadata = std::fs::symlink_metadata(&path)?;
let is_all_all = true;
let parent_dir = Some(parent_dir);
Ok(File { path, parent_dir: Some(parent_dir), metadata, ext, name: "..".to_string(), is_all_all })
Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all })
}
/// A files name is derived from its string. This needs to handle directories
@ -127,7 +129,9 @@ impl<'dir> File<'dir> {
fn ext(path: &Path) -> Option<String> {
let name = path.file_name().map(|f| f.to_string_lossy().to_string())?;
name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
name.rfind('.')
.map(|p| name[p + 1 ..]
.to_ascii_lowercase())
}
/// Whether this file is a directory on the filesystem.
@ -157,7 +161,7 @@ impl<'dir> File<'dir> {
///
/// Returns an IO error upon failure, but this shouldnt 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> {
pub fn to_dir(&self) -> io::Result<Dir> {
Dir::read_dir(self.path.clone())
}
@ -249,7 +253,8 @@ impl<'dir> File<'dir> {
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 }))
let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false };
FileTarget::Ok(Box::new(file))
}
Err(e) => {
error!("Error following link {:?}: {:#?}", &path, e);
@ -274,14 +279,14 @@ impl<'dir> File<'dir> {
}
}
/// This file's inode.
/// This files inode.
pub fn inode(&self) -> f::Inode {
f::Inode(self.metadata.ino())
}
/// This file's number of filesystem blocks.
/// This files number of filesystem blocks.
///
/// (Not the size of each block, which we don't actually report on)
/// (Not the size of each block, which we dont actually report on)
pub fn blocks(&self) -> f::Blocks {
if self.is_file() || self.is_link() {
f::Blocks::Some(self.metadata.blocks())
@ -334,17 +339,19 @@ impl<'dir> File<'dir> {
pub fn changed_time(&self) -> Option<SystemTime> {
let (mut sec, mut nsec) = (self.metadata.ctime(), self.metadata.ctime_nsec());
Some(
if sec < 0 {
if nsec > 0 {
sec += 1;
nsec = nsec - 1_000_000_000;
}
UNIX_EPOCH - Duration::new(sec.abs() as u64, nsec.abs() as u32)
} else {
UNIX_EPOCH + Duration::new(sec as u64, nsec as u32)
}
)
if sec < 0 {
if nsec > 0 {
sec += 1;
nsec -= 1_000_000_000;
}
let duration = Duration::new(sec.abs() as u64, nsec.abs() as u32);
Some(UNIX_EPOCH - duration)
}
else {
let duration = Duration::new(sec as u64, nsec as u32);
Some(UNIX_EPOCH + duration)
}
}
/// This files last accessed timestamp, if available on this platform.
@ -392,7 +399,7 @@ impl<'dir> File<'dir> {
/// This files permissions, with flags for each bit.
pub fn permissions(&self) -> f::Permissions {
let bits = self.metadata.mode();
let has_bit = |bit| { bits & bit == bit };
let has_bit = |bit| bits & bit == bit;
f::Permissions {
user_read: has_bit(modes::USER_READ),
@ -417,13 +424,13 @@ impl<'dir> File<'dir> {
///
/// This will always return `false` if the file has no extension.
pub fn extension_is_one_of(&self, choices: &[&str]) -> bool {
match self.ext {
Some(ref ext) => choices.contains(&&ext[..]),
None => false,
match &self.ext {
Some(ext) => choices.contains(&&ext[..]),
None => false,
}
}
/// Whether this file's name, including extension, is any of the strings
/// Whether this files name, including extension, is any of the strings
/// that get passed in.
pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
choices.contains(&&self.name[..])
@ -451,11 +458,11 @@ pub enum FileTarget<'dir> {
/// There was an IO error when following the link. This can happen if the
/// file isnt a link to begin with, but also if, say, we dont have
/// permission to follow it.
Err(IOError),
Err(io::Error),
// Err is its own variant, instead of having the whole thing be inside an
// `IOResult`, because being unable to follow a symlink is not a serious
// error -- we just display the error message and move on.
// `io::Result`, because being unable to follow a symlink is not a serious
// error we just display the error message and move on.
}
impl<'dir> FileTarget<'dir> {
@ -463,10 +470,7 @@ impl<'dir> FileTarget<'dir> {
/// Whether this link doesnt lead to a file, for whatever reason. This
/// gets used to determine how to highlight the link in grid views.
pub fn is_broken(&self) -> bool {
match *self {
FileTarget::Ok(_) => false,
FileTarget::Broken(_) | FileTarget::Err(_) => true,
}
matches!(self, Self::Broken(_) | Self::Err(_))
}
}
@ -474,9 +478,10 @@ impl<'dir> FileTarget<'dir> {
/// More readable aliases for the permission bits exposed by libc.
#[allow(trivial_numeric_casts)]
mod modes {
pub type Mode = u32;
// The `libc::mode_t` types actual type varies, but the value returned
// from `metadata.permissions().mode()` is always `u32`.
pub type Mode = u32;
pub const USER_READ: Mode = libc::S_IRUSR as Mode;
pub const USER_WRITE: Mode = libc::S_IWUSR as Mode;

View File

@ -5,8 +5,8 @@ use std::iter::FromIterator;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use crate::fs::File;
use crate::fs::DotFilter;
use crate::fs::File;
/// The **file filter** processes a list of files before displaying them to
@ -88,15 +88,14 @@ pub struct FileFilter {
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));
files.retain(|f| ! self.ignore_patterns.is_ignored(&f.name));
if self.only_dirs {
files.retain(|f| f.is_directory());
files.retain(File::is_directory);
}
}
@ -110,14 +109,18 @@ impl FileFilter {
/// `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));
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()));
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();
@ -126,8 +129,9 @@ impl FileFilter {
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().points_to_directory()
.cmp(&a.as_ref().points_to_directory())
files.sort_by(|a, b| {
b.as_ref().points_to_directory()
.cmp(&a.as_ref().points_to_directory())
});
}
}
@ -175,13 +179,13 @@ pub enum SortField {
/// The time the file was changed (the “ctime”).
///
/// This field is used to mark the time when a files metadata
/// changed -- its permissions, owners, or link count.
/// 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").
/// The time the file was created (the “btime” or “birthtime”).
CreatedDate,
/// The type of the file: directories, links, pipes, regular, files, etc.
@ -240,51 +244,48 @@ impl SortField {
use self::SortCase::{ABCabc, AaBbCc};
match self {
SortField::Unsorted => Ordering::Equal,
Self::Unsorted => Ordering::Equal,
SortField::Name(ABCabc) => natord::compare(&a.name, &b.name),
SortField::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name),
Self::Name(ABCabc) => natord::compare(&a.name, &b.name),
Self::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
Self::Size => a.metadata.len().cmp(&b.metadata.len()),
Self::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
Self::ModifiedDate => a.modified_time().cmp(&b.modified_time()),
Self::AccessedDate => a.accessed_time().cmp(&b.accessed_time()),
Self::ChangedDate => a.changed_time().cmp(&b.changed_time()),
Self::CreatedDate => a.created_time().cmp(&b.created_time()),
Self::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
Self::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) {
Self::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) {
Self::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)
Self::NameMixHidden(ABCabc) => natord::compare(
Self::strip_dot(&a.name),
Self::strip_dot(&b.name)
),
SortField::NameMixHidden(AaBbCc) => natord::compare_ignore_case(
SortField::strip_dot(&a.name),
SortField::strip_dot(&b.name)
Self::NameMixHidden(AaBbCc) => natord::compare_ignore_case(
Self::strip_dot(&a.name),
Self::strip_dot(&b.name)
)
}
}
fn strip_dot(n: &str) -> &str {
if n.starts_with('.') {
&n[1..]
} else {
n
}
if n.starts_with('.') { &n[1..] }
else { n }
}
}
@ -298,16 +299,20 @@ pub struct IgnorePatterns {
}
impl FromIterator<glob::Pattern> for IgnorePatterns {
fn from_iter<I: IntoIterator<Item = glob::Pattern>>(iter: I) -> Self {
IgnorePatterns { patterns: iter.into_iter().collect() }
fn from_iter<I>(iter: I) -> Self
where I: IntoIterator<Item = glob::Pattern>
{
let patterns = iter.into_iter().collect();
Self { patterns }
}
}
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 dont
/// parse correctly are returned separately.
/// are valid glob patterns into an `IgnorePatterns`. The inputs that
/// dont 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();
@ -328,12 +333,12 @@ impl IgnorePatterns {
}
}
(IgnorePatterns { patterns }, errors)
(Self { patterns }, errors)
}
/// Create a new empty set of patterns that matches nothing.
pub fn empty() -> IgnorePatterns {
IgnorePatterns { patterns: Vec::new() }
pub fn empty() -> Self {
Self { patterns: Vec::new() }
}
/// Test whether the given file should be hidden from the results.
@ -371,7 +376,6 @@ pub enum GitIgnore {
// > usually found in $XDG_CONFIG_HOME/git/ignore.
#[cfg(test)]
mod test_ignores {
use super::*;

View File

@ -4,7 +4,7 @@ pub use self::dir::{Dir, DotFilter};
mod file;
pub use self::file::{File, FileTarget};
pub mod dir_action;
pub mod feature;
pub mod fields;
pub mod filter;
pub mod dir_action;

View File

@ -124,11 +124,17 @@ impl FileIcon for FileExtensions {
fn icon_file(&self, file: &File) -> Option<char> {
use crate::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,
})
if self.is_music(file) || self.is_lossless(file) {
Some(Icons::Audio.value())
}
else if self.is_image(file) {
Some(Icons::Image.value())
}
else if self.is_video(file) {
Some(Icons::Video.value())
}
else {
None
}
}
}

View File

@ -11,10 +11,10 @@ impl<'a> File<'a> {
/// The point of this is to highlight compiled files such as `foo.js` when
/// their source file `foo.coffee` exists in the same directory.
/// For example, `foo.js` is perfectly valid without `foo.coffee`, so we
/// don't want to always blindly highlight `*.js` as compiled.
/// dont want to always blindly highlight `*.js` as compiled.
/// (See also `FileExtensions#is_compiled`)
pub fn get_source_files(&self) -> Vec<PathBuf> {
if let Some(ref ext) = self.ext {
if let Some(ext) = &self.ext {
match &ext[..] {
"css" => vec![self.path.with_extension("sass"), self.path.with_extension("less")], // SASS, Less
"js" => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")], // CoffeeScript, TypeScript
@ -34,7 +34,7 @@ impl<'a> File<'a> {
}
}
else {
vec![] // No source files if there's no extension, either!
vec![] // No source files if theres no extension, either!
}
}
}

66
src/logger.rs Normal file
View File

@ -0,0 +1,66 @@
//! Debug error logging.
use std::ffi::OsStr;
use ansi_term::{Colour, ANSIString};
/// Sets the internal logger, changing the log level based on the value of an
/// environment variable.
pub fn configure<T: AsRef<OsStr>>(ev: Option<T>) {
let ev = match ev {
Some(v) => v,
None => return,
};
let env_var = ev.as_ref();
if env_var.is_empty() {
return;
}
if env_var == "trace" {
log::set_max_level(log::LevelFilter::Trace);
}
else {
log::set_max_level(log::LevelFilter::Debug);
}
let result = log::set_logger(GLOBAL_LOGGER);
if let Err(e) = result {
eprintln!("Failed to initialise logger: {}", e);
}
}
#[derive(Debug)]
struct Logger;
const GLOBAL_LOGGER: &Logger = &Logger;
impl log::Log for Logger {
fn enabled(&self, _: &log::Metadata<'_>) -> bool {
true // no need to filter after using set_max_level.
}
fn log(&self, record: &log::Record<'_>) {
let open = Colour::Fixed(243).paint("[");
let level = level(record.level());
let close = Colour::Fixed(243).paint("]");
eprintln!("{}{} {}{} {}", open, level, record.target(), close, record.args());
}
fn flush(&self) {
// no need to flush with eprintln!.
}
}
fn level(level: log::Level) -> ANSIString<'static> {
match level {
log::Level::Error => Colour::Red.paint("ERROR"),
log::Level::Warn => Colour::Yellow.paint("WARN"),
log::Level::Info => Colour::Cyan.paint("INFO"),
log::Level::Debug => Colour::Blue.paint("DEBUG"),
log::Level::Trace => Colour::Fixed(245).paint("TRACE"),
}
}

280
src/main.rs Normal file
View File

@ -0,0 +1,280 @@
#![warn(trivial_casts, trivial_numeric_casts)]
#![warn(unused_results)]
use std::env;
use std::ffi::{OsStr, OsString};
use std::io::{self, Write, ErrorKind};
use std::path::{Component, PathBuf};
use ansi_term::{ANSIStrings, Style};
use log::*;
use crate::fs::{Dir, File};
use crate::fs::feature::git::GitCache;
use crate::fs::filter::GitIgnore;
use crate::options::{Options, Vars, vars, OptionsResult};
use crate::output::{escape, lines, grid, grid_details, details, View, Mode};
mod fs;
mod info;
mod logger;
mod options;
mod output;
mod style;
fn main() {
use std::process::exit;
logger::configure(env::var_os(vars::EXA_DEBUG));
let args: Vec<_> = env::args_os().skip(1).collect();
match Options::parse(args.iter().map(|e| e.as_ref()), &LiveVars) {
OptionsResult::Ok(options, mut input_paths) => {
// List the current directory by default.
// (This has to be done here, otherwise git_options wont see it.)
if input_paths.is_empty() {
input_paths = vec![ OsStr::new(".") ];
}
let git = git_options(&options, &input_paths);
let writer = io::stdout();
let exa = Exa { options, writer, input_paths, git };
match exa.run() {
Ok(exit_status) => {
exit(exit_status);
}
Err(e) if e.kind() == ErrorKind::BrokenPipe => {
warn!("Broken pipe error: {}", e);
exit(exits::SUCCESS);
}
Err(e) => {
eprintln!("{}", e);
exit(exits::RUNTIME_ERROR);
}
}
}
OptionsResult::Help(help_text) => {
println!("{}", help_text);
}
OptionsResult::Version(version_str) => {
println!("{}", version_str);
}
OptionsResult::InvalidOptions(error) => {
eprintln!("{}", error);
if let Some(s) = error.suggestion() {
eprintln!("{}", s);
}
exit(exits::OPTIONS_ERROR);
}
}
}
/// The main program wrapper.
pub struct Exa<'args> {
/// List of command-line options, having been successfully parsed.
pub options: Options,
/// The output handle that we write to.
pub writer: io::Stdout,
/// List of the free command-line arguments that should correspond to file
/// names (anything that isnt an option).
pub input_paths: 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>,
}
/// 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> {
env::var_os(name)
}
}
/// Create a Git cache populated with the arguments that are going to be
/// listed before theyre 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
}
}
impl<'args> Exa<'args> {
pub fn run(mut self) -> io::Result<i32> {
debug!("Running with options: {:#?}", self.options);
let mut files = Vec::new();
let mut dirs = Vec::new();
let mut exit_status = 0;
for file_path in &self.input_paths {
match File::from_args(PathBuf::from(file_path), None, None) {
Err(e) => {
exit_status = 2;
writeln!(io::stderr(), "{:?}: {}", file_path, e)?;
}
Ok(f) => {
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!(io::stderr(), "{:?}: {}", file_path, e)?,
}
}
else {
files.push(f);
}
}
}
}
// We want to print a directorys name before we list it, *except* in
// the case where its the only directory, *except* if there are any
// files to print as well. (Its a double negative)
let no_files = files.is_empty();
let is_only_dir = dirs.len() == 1 && no_files;
self.options.filter.filter_argument_files(&mut files);
self.print_files(None, files)?;
self.print_dirs(dirs, no_files, is_only_dir, exit_status)
}
fn print_dirs(&mut self, dir_files: Vec<Dir>, mut first: bool, is_only_dir: bool, exit_status: i32) -> io::Result<i32> {
for dir in dir_files {
// Put a gap between directories, or between the list of files and
// the first directory.
if first {
first = false;
}
else {
writeln!(&mut self.writer)?;
}
if ! is_only_dir {
let mut bits = Vec::new();
escape(dir.path.display().to_string(), &mut bits, Style::default(), Style::default());
writeln!(&mut self.writer, "{}:", ANSIStrings(&bits))?;
}
let mut children = Vec::new();
let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
for file in dir.files(self.options.filter.dot_filter, self.git.as_ref(), git_ignore) {
match file {
Ok(file) => children.push(file),
Err((path, e)) => writeln!(io::stderr(), "[{}: {}]", path.display(), e)?,
}
};
self.options.filter.filter_child_files(&mut children);
self.options.filter.sort_files(&mut children);
if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1;
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() && ! f.is_all_all) {
match child_dir.to_dir() {
Ok(d) => child_dirs.push(d),
Err(e) => writeln!(io::stderr(), "{}: {}", child_dir.path.display(), e)?,
}
}
self.print_files(Some(&dir), children)?;
match self.print_dirs(child_dirs, false, false, exit_status) {
Ok(_) => (),
Err(e) => return Err(e),
}
continue;
}
}
self.print_files(Some(&dir), children)?;
}
Ok(exit_status)
}
/// Prints the list of files using whichever view is selected.
/// For various annoying logistical reasons, each one handles
/// printing differently...
fn print_files(&mut self, dir: Option<&Dir>, files: Vec<File>) -> io::Result<()> {
if files.is_empty() {
return Ok(());
}
let View { ref mode, ref colours, ref style } = self.options.view;
match mode {
Mode::Lines(ref opts) => {
let r = lines::Render { files, colours, style, opts };
r.render(&mut self.writer)
}
Mode::Grid(ref opts) => {
let r = grid::Render { files, colours, style, opts };
r.render(&mut self.writer)
}
Mode::Details(ref opts) => {
let filter = &self.options.filter;
let recurse = self.options.dir_action.recurse_options();
let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
let git = self.git.as_ref();
let r = details::Render { dir, files, colours, style, opts, filter, recurse, git_ignoring, git };
r.render(&mut 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 git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
let git = self.git.as_ref();
let r = grid_details::Render { dir, files, colours, style, grid, details, filter, row_threshold, git_ignoring, git };
r.render(&mut self.writer)
}
}
}
}
mod exits {
/// Exit code for when exa runs OK.
pub const SUCCESS: i32 = 0;
/// Exit code for when there was at least one I/O error during execution.
pub const RUNTIME_ERROR: i32 = 1;
/// Exit code for when the command-line options are invalid.
pub const OPTIONS_ERROR: i32 = 3;
}

View File

@ -1,7 +1,7 @@
//! Parsing the options for `DirAction`.
use crate::options::parser::MatchedFlags;
use crate::options::{flags, Misfire};
use crate::options::{flags, OptionsError};
use crate::fs::dir_action::{DirAction, RecurseOptions};
@ -12,35 +12,35 @@ impl DirAction {
/// 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> {
pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let recurse = matches.has(&flags::RECURSE)?;
let as_file = matches.has(&flags::LIST_DIRS)?;
let tree = matches.has(&flags::TREE)?;
if matches.is_strict() {
// Early check for --level when it wouldnt do anything
if !recurse && !tree && matches.count(&flags::LEVEL) > 0 {
return Err(Misfire::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
if ! recurse && ! tree && matches.count(&flags::LEVEL) > 0 {
return Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
}
else if recurse && as_file {
return Err(Misfire::Conflict(&flags::RECURSE, &flags::LIST_DIRS));
return Err(OptionsError::Conflict(&flags::RECURSE, &flags::LIST_DIRS));
}
else if tree && as_file {
return Err(Misfire::Conflict(&flags::TREE, &flags::LIST_DIRS));
return Err(OptionsError::Conflict(&flags::TREE, &flags::LIST_DIRS));
}
}
if tree {
Ok(DirAction::Recurse(RecurseOptions::deduce(matches, true)?))
Ok(Self::Recurse(RecurseOptions::deduce(matches, true)?))
}
else if recurse {
Ok(DirAction::Recurse(RecurseOptions::deduce(matches, false)?))
Ok(Self::Recurse(RecurseOptions::deduce(matches, false)?))
}
else if as_file {
Ok(DirAction::AsFile)
Ok(Self::AsFile)
}
else {
Ok(DirAction::List)
Ok(Self::List)
}
}
}
@ -52,18 +52,18 @@ impl RecurseOptions {
/// flags 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 isnt.
pub fn deduce(matches: &MatchedFlags, tree: bool) -> Result<RecurseOptions, Misfire> {
pub fn deduce(matches: &MatchedFlags, tree: bool) -> Result<Self, OptionsError> {
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)),
Err(e) => return Err(OptionsError::FailedParse(e)),
}
}
else {
None
};
Ok(RecurseOptions { tree, max_depth })
Ok(Self { tree, max_depth })
}
}
@ -115,12 +115,12 @@ mod test {
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)));
test!(dirs_recurse_2: DirAction <- ["--list-dirs", "--recurse"]; Complain => Err(OptionsError::Conflict(&flags::RECURSE, &flags::LIST_DIRS)));
test!(dirs_tree_2: DirAction <- ["--list-dirs", "--tree"]; Complain => Err(OptionsError::Conflict(&flags::TREE, &flags::LIST_DIRS)));
test!(just_level_2: DirAction <- ["--level=4"]; Complain => Err(OptionsError::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'))));
test!(overriding_2: DirAction <- ["-RL=6", "-L=7"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'L'), Flag::Short(b'L'))));
}

109
src/options/error.rs Normal file
View File

@ -0,0 +1,109 @@
use std::ffi::OsString;
use std::fmt;
use std::num::ParseIntError;
use crate::options::flags;
use crate::options::parser::{Arg, Flag, ParseError};
/// Something wrong with the combination of options the user has picked.
#[derive(PartialEq, Debug)]
pub enum OptionsError {
/// There was an error (from `getopts`) parsing the arguments.
Parse(ParseError),
/// The user supplied an illegal choice to an Argument.
BadArgument(&'static Arg, OsString),
/// The user supplied a set of options
Unsupported(String),
/// An option was given twice or more in strict mode.
Duplicate(Flag, Flag),
/// Two options were given that conflict with one another.
Conflict(&'static Arg, &'static Arg),
/// An option was given that does nothing when another one either is or
/// isnt present.
Useless(&'static Arg, bool, &'static Arg),
/// An option was given that does nothing when either of two other options
/// are not present.
Useless2(&'static Arg, &'static Arg, &'static Arg),
/// A very specific edge case where --tree cant be used with --all twice.
TreeAllAll,
/// A numeric option was given that failed to be parsed as a number.
FailedParse(ParseIntError),
/// A glob ignore was given that failed to be parsed as a pattern.
FailedGlobPattern(String),
}
impl From<glob::PatternError> for OptionsError {
fn from(error: glob::PatternError) -> Self {
Self::FailedGlobPattern(error.to_string())
}
}
impl fmt::Display for OptionsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use crate::options::parser::TakesValue;
match self {
Self::BadArgument(arg, 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)
}
}
Self::Parse(e) => write!(f, "{}", e),
Self::Unsupported(e) => write!(f, "{}", e),
Self::Conflict(a, b) => write!(f, "Option {} conflicts with option {}", a, b),
Self::Duplicate(a, b) if a == b => write!(f, "Flag {} was given twice", a),
Self::Duplicate(a, b) => write!(f, "Flag {} conflicts with flag {}", a, b),
Self::Useless(a, false, b) => write!(f, "Option {} is useless without option {}", a, b),
Self::Useless(a, true, b) => write!(f, "Option {} is useless given option {}", a, b),
Self::Useless2(a, b1, b2) => write!(f, "Option {} is useless without options {} or {}", a, b1, b2),
Self::TreeAllAll => write!(f, "Option --tree is useless given --all --all"),
Self::FailedParse(ref e) => write!(f, "Failed to parse number: {}", e),
Self::FailedGlobPattern(ref e) => write!(f, "Failed to parse glob pattern: {}", e),
}
}
}
impl OptionsError {
/// 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 {
Self::BadArgument(time, r) if *time == &flags::TIME && r == "r" => {
Some("To sort oldest files last, try \"--sort oldest\", or just \"-sold\"")
}
Self::Parse(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(pub &'static [&'static str]);
impl fmt::Display for Choices {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "choices: {}", self.0.join(", "))
}
}

View File

@ -3,22 +3,22 @@
use crate::fs::DotFilter;
use crate::fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore};
use crate::options::{flags, Misfire};
use crate::options::{flags, OptionsError};
use crate::options::parser::MatchedFlags;
impl FileFilter {
/// Determines which of all the file filter options to use.
pub fn deduce(matches: &MatchedFlags) -> Result<FileFilter, Misfire> {
Ok(FileFilter {
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)?,
pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
Ok(Self {
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)?,
})
}
}
@ -29,42 +29,77 @@ impl SortField {
/// This arguments value can be one of several flags, listed above.
/// Returns the default sort field if none is given, or `Err` if the
/// value doesnt correspond to a sort field we know about.
fn deduce(matches: &MatchedFlags) -> Result<SortField, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let word = match matches.get(&flags::SORT)? {
Some(w) => w,
None => return Ok(SortField::default()),
None => return Ok(Self::default()),
};
// Get String because we cant match an OsStr
let word = match word.to_str() {
Some(ref w) => *w,
None => return Err(Misfire::BadArgument(&flags::SORT, word.into()))
Some(w) => w,
None => return Err(OptionsError::BadArgument(&flags::SORT, word.into()))
};
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),
"name" | "filename" => {
Self::Name(SortCase::AaBbCc)
}
"Name" | "Filename" => {
Self::Name(SortCase::ABCabc)
}
".name" | ".filename" => {
Self::NameMixHidden(SortCase::AaBbCc)
}
".Name" | ".Filename" => {
Self::NameMixHidden(SortCase::ABCabc)
}
"size" | "filesize" => {
Self::Size
}
"ext" | "extension" => {
Self::Extension(SortCase::AaBbCc)
}
"Ext" | "Extension" => {
Self::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, doesnt it?
"date" | "time" | "mod" | "modified" | "new" | "newest" => SortField::ModifiedDate,
"date" | "time" | "mod" | "modified" | "new" | "newest" => {
Self::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()))
"age" | "old" | "oldest" => {
Self::ModifiedAge
}
"ch" | "changed" => {
Self::ChangedDate
}
"acc" | "accessed" => {
Self::AccessedDate
}
"cr" | "created" => {
Self::CreatedDate
}
"inode" => {
Self::FileInode
}
"type" => {
Self::FileType
}
"none" => {
Self::Unsorted
}
_ => {
return Err(OptionsError::BadArgument(&flags::SORT, word.into()));
}
};
Ok(field)
@ -103,10 +138,9 @@ impl SortField {
// “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)
fn default() -> Self {
Self::Name(SortCase::AaBbCc)
}
}
@ -119,23 +153,23 @@ impl DotFilter {
/// It also checks for the `--tree` option in strict mode, because of a
/// special case where `--tree --all --all` wont work: listing the
/// parent directory in tree mode would loop onto itself!
pub fn deduce(matches: &MatchedFlags) -> Result<DotFilter, Misfire> {
pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let count = matches.count(&flags::ALL);
if count == 0 {
Ok(DotFilter::JustFiles)
Ok(Self::JustFiles)
}
else if count == 1 {
Ok(DotFilter::Dotfiles)
Ok(Self::Dotfiles)
}
else if matches.count(&flags::TREE) > 0 {
Err(Misfire::TreeAllAll)
Err(OptionsError::TreeAllAll)
}
else if count >= 3 && matches.is_strict() {
Err(Misfire::Conflict(&flags::ALL, &flags::ALL))
Err(OptionsError::Conflict(&flags::ALL, &flags::ALL))
}
else {
Ok(DotFilter::DotfilesAndDots)
Ok(Self::DotfilesAndDots)
}
}
}
@ -146,38 +180,41 @@ impl IgnorePatterns {
/// Determines the set of glob patterns to use based on the
/// `--ignore-glob` arguments value. This is a list of strings
/// separated by pipe (`|`) characters, given in any order.
pub fn deduce(matches: &MatchedFlags) -> Result<IgnorePatterns, Misfire> {
pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
// If there are no inputs, we return a set of patterns that doesnt
// match anything, rather than, say, `None`.
let inputs = match matches.get(&flags::IGNORE_GLOB)? {
None => return Ok(IgnorePatterns::empty()),
Some(is) => is,
Some(is) => is,
None => return Ok(Self::empty()),
};
// 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('|'));
let (patterns, mut errors) = Self::parse_from_iter(inputs.to_string_lossy().split('|'));
// 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),
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 })
pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
if matches.has(&flags::GIT_IGNORE)? {
Ok(Self::CheckAndIgnore)
}
else {
Ok(Self::Off)
}
}
}
#[cfg(test)]
mod test {
use super::*;
@ -223,13 +260,13 @@ mod test {
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"))));
test!(error: SortField <- ["--sort=colour"]; Both => Err(OptionsError::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"))));
test!(overridden_3: SortField <- ["--sort=cr", "--sort", "mod"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
}
@ -245,12 +282,12 @@ mod test {
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)));
test!(all_all_4: DotFilter <- ["-aaa"]; Complain => Err(OptionsError::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));
test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(OptionsError::TreeAllAll));
test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(OptionsError::TreeAllAll));
}
@ -272,8 +309,8 @@ mod test {
// 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'))));
test!(overridden_3: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
}

View File

@ -1,4 +1,4 @@
use crate::options::parser::{Arg, Args, Values, TakesValue};
use crate::options::parser::{Arg, Args, TakesValue, Values};
// exa options
@ -32,8 +32,8 @@ pub static GIT_IGNORE: Arg = Arg { short: None, long: "git-ignore", t
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" ];
"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 };

View File

@ -1,8 +1,8 @@
use std::fmt;
use crate::fs::feature::xattr;
use crate::options::flags;
use crate::options::parser::MatchedFlags;
use crate::fs::feature::xattr;
static OPTIONS: &str = r##"
@ -64,7 +64,7 @@ static OCTAL_HELP: &str = r##" --octal-permissions list each file's permissi
/// 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 sections help.
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct HelpString {
/// Only show the help for the long section, not all the help.
@ -86,15 +86,15 @@ impl HelpString {
/// We dont do any strict-mode error checking here: its 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> {
pub fn deduce(matches: &MatchedFlags) -> Option<Self> {
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 })
Some(Self { only_long, git, xattrs })
}
else {
Ok(()) // no help needs to be shown
None
}
}
}
@ -106,7 +106,7 @@ impl fmt::Display for HelpString {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
writeln!(f, "Usage:\n exa [options] [files...]")?;
if !self.only_long {
if ! self.only_long {
write!(f, "{}", OPTIONS)?;
}
@ -127,36 +127,29 @@ impl fmt::Display for HelpString {
}
#[cfg(test)]
mod test {
use crate::options::Options;
use std::ffi::OsString;
fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
}
use crate::options::{Options, OptionsResult};
use std::ffi::OsStr;
#[test]
fn help() {
let args = [ os("--help") ];
let opts = Options::parse(&args, &None);
assert!(opts.is_err())
let args = vec![ OsStr::new("--help") ];
let opts = Options::parse(args, &None);
assert!(matches!(opts, OptionsResult::Help(_)));
}
#[test]
fn help_with_file() {
let args = [ os("--help"), os("me") ];
let opts = Options::parse(&args, &None);
assert!(opts.is_err())
let args = vec![ OsStr::new("--help"), OsStr::new("me") ];
let opts = Options::parse(args, &None);
assert!(matches!(opts, OptionsResult::Help(_)));
}
#[test]
fn unhelpful() {
let args = [];
let opts = Options::parse(&args, &None);
assert!(opts.is_ok()) // no help when --help isnt passed
let args = vec![];
let opts = Options::parse(args, &None);
assert!(! matches!(opts, OptionsResult::Help(_))) // no help when --help isnt passed
}
}

View File

@ -1,141 +0,0 @@
use std::ffi::OsString;
use std::fmt;
use std::num::ParseIntError;
use crate::options::{flags, HelpString, VersionString};
use crate::options::parser::{Arg, Flag, ParseError};
/// A **misfire** is a thing that can happen instead of listing files -- a
/// catch-all for anything outside the programs normal execution.
#[derive(PartialEq, Debug)]
pub enum Misfire {
/// The getopts crate didnt like these Arguments.
InvalidOptions(ParseError),
/// 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 isnt strictly an error, which is why
/// this enum isnt named Error!
Help(HelpString),
/// The user wanted the version number.
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 Arg, &'static Arg),
/// An option was given that does nothing when another one either is or
/// isn't present.
Useless(&'static Arg, bool, &'static Arg),
/// An option was given that does nothing when either of two other options
/// are not present.
Useless2(&'static Arg, &'static Arg, &'static Arg),
/// A very specific edge case where --tree cant be used with --all twice.
TreeAllAll,
/// A numeric option was given that failed to be parsed as a number.
FailedParse(ParseIntError),
/// A glob ignore was given that failed to be parsed as a pattern.
FailedGlobPattern(String),
}
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,
}
}
}
impl From<glob::PatternError> for Misfire {
fn from(error: glob::PatternError) -> Misfire {
Misfire::FailedGlobPattern(error.to_string())
}
}
impl fmt::Display for Misfire {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use crate::options::parser::TakesValue;
use self::Misfire::*;
match *self {
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(", "))
}
}

View File

@ -60,7 +60,7 @@
//!
//! `--sort=size` should override `--sort=Name` because its closer to the end
//! of the arguments array. In fact, because theres no way to tell where the
//! arguments came from -- its just a heuristic -- this will still work even
//! arguments came from — its just a heuristic — this will still work even
//! if no aliases are being used!
//!
//! Finally, this isnt just useful when options could override each other.
@ -69,32 +69,32 @@
//! its clear what the user wants.
use std::ffi::{OsStr, OsString};
use std::ffi::OsStr;
use crate::fs::dir_action::DirAction;
use crate::fs::filter::{FileFilter,GitIgnore};
use crate::fs::filter::{FileFilter, GitIgnore};
use crate::output::{View, Mode, details, grid_details};
mod style;
mod dir_action;
mod filter;
mod flags;
mod style;
mod view;
mod error;
pub use self::error::OptionsError;
mod help;
use self::help::HelpString;
mod version;
use self::version::VersionString;
mod misfire;
pub use self::misfire::Misfire;
mod parser;
use self::parser::MatchedFlags;
pub mod vars;
pub use self::vars::Vars;
mod parser;
mod flags;
use self::parser::MatchedFlags;
mod version;
use self::version::VersionString;
/// These **options** represent a parsed, error-checked versions of the
@ -119,9 +119,10 @@ impl Options {
/// struct and a list of free filenames, using the environment variables
/// for extra options.
#[allow(unused_results)]
pub fn parse<'args, I, V>(args: I, vars: &V) -> Result<(Options, Vec<&'args OsStr>), Misfire>
where I: IntoIterator<Item=&'args OsString>,
V: Vars {
pub fn parse<'args, I, V>(args: I, vars: &V) -> OptionsResult<'args>
where I: IntoIterator<Item = &'args OsStr>,
V: Vars,
{
use crate::options::parser::{Matches, Strictness};
let strictness = match vars.get(vars::EXA_STRICT) {
@ -131,15 +132,22 @@ impl Options {
};
let Matches { flags, frees } = match flags::ALL_ARGS.parse(args, strictness) {
Ok(m) => m,
Err(e) => return Err(Misfire::InvalidOptions(e)),
Ok(m) => m,
Err(pe) => return OptionsResult::InvalidOptions(OptionsError::Parse(pe)),
};
HelpString::deduce(&flags).map_err(Misfire::Help)?;
VersionString::deduce(&flags).map_err(Misfire::Version)?;
if let Some(help) = HelpString::deduce(&flags) {
return OptionsResult::Help(help);
}
let options = Options::deduce(&flags, vars)?;
Ok((options, frees))
if let Some(version) = VersionString::deduce(&flags) {
return OptionsResult::Version(version);
}
match Self::deduce(&flags, vars) {
Ok(options) => OptionsResult::Ok(options, frees),
Err(oe) => OptionsResult::InvalidOptions(oe),
}
}
/// Whether the View specified in this set of options includes a Git
@ -159,22 +167,38 @@ impl Options {
/// Determines the complete set of options based on the given command-line
/// arguments, after theyve been parsed.
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Options, Misfire> {
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
let dir_action = DirAction::deduce(matches)?;
let filter = FileFilter::deduce(matches)?;
let view = View::deduce(matches, vars)?;
Ok(Options { dir_action, view, filter })
Ok(Self { dir_action, view, filter })
}
}
/// The result of the `Options::getopts` function.
#[derive(Debug)]
pub enum OptionsResult<'args> {
/// The options were parsed successfully.
Ok(Options, Vec<&'args OsStr>),
/// There was an error parsing the arguments.
InvalidOptions(OptionsError),
/// One of the arguments was `--help`, so display help.
Help(HelpString),
/// One of the arguments was `--version`, so display the version number.
Version(VersionString),
}
#[cfg(test)]
pub mod test {
use super::{Options, Misfire, flags};
use crate::options::parser::{Arg, MatchedFlags};
use std::ffi::OsString;
use std::ffi::OsStr;
#[derive(PartialEq, Debug)]
pub enum Strictnesses {
@ -195,55 +219,19 @@ pub mod test {
use self::Strictnesses::*;
use crate::options::parser::{Args, Strictness};
let bits = inputs.into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
let bits = inputs.into_iter().map(OsStr::new).collect::<Vec<_>>();
let mut result = Vec::new();
if strictnesses == Last || strictnesses == Both {
let results = Args(args).parse(bits.iter(), Strictness::UseLastArguments);
let results = Args(args).parse(bits.clone(), Strictness::UseLastArguments);
result.push(get(&results.unwrap().flags));
}
if strictnesses == Complain || strictnesses == Both {
let results = Args(args).parse(bits.iter(), Strictness::ComplainAboutRedundantArguments);
let results = Args(args).parse(bits, Strictness::ComplainAboutRedundantArguments);
result.push(get(&results.unwrap().flags));
}
result
}
/// 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 = [ 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<OsString> = Vec::new();
let outs = Options::parse(&nothing, &None).unwrap().1;
assert!(outs.is_empty()); // Listing the `.` directory is done in main.rs
}
#[test]
fn long_across() {
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 args = [ os("--oneline"), os("--across") ];
let opts = Options::parse(&args, &None);
assert_eq!(opts.unwrap_err(), Misfire::Useless(&flags::ACROSS, true, &flags::ONE_LINE))
}
}

View File

@ -31,16 +31,16 @@
use std::ffi::{OsStr, OsString};
use std::fmt;
use crate::options::Misfire;
use crate::options::error::{OptionsError, Choices};
/// 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 dont actually
/// store the users input after its been matched to a flag, we just store
/// which flag it was.
/// the arguments will all be unchecked `OsString` values, because we dont
/// actually store the users input after its 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
@ -52,7 +52,7 @@ pub type Values = &'static [&'static str];
/// A **flag** is either of the two argument types, because they have to
/// be in the same array together.
#[derive(PartialEq, Debug, Clone)]
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Flag {
Short(ShortArg),
Long(LongArg),
@ -60,18 +60,18 @@ pub enum Flag {
impl Flag {
pub fn matches(&self, arg: &Arg) -> bool {
match *self {
Flag::Short(short) => arg.short == Some(short),
Flag::Long(long) => arg.long == long,
match self {
Self::Short(short) => arg.short == Some(*short),
Self::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),
match self {
Self::Short(short) => write!(f, "-{}", *short as char),
Self::Long(long) => write!(f, "--{}", long),
}
}
}
@ -108,7 +108,7 @@ pub enum TakesValue {
/// An **argument** can be matched by one of the users input strings.
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Arg {
/// The short argument that matches it, if any.
@ -144,9 +144,9 @@ 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> {
where I: IntoIterator<Item = &'args OsStr>
{
use std::os::unix::ffi::OsStrExt;
use self::TakesValue::*;
let mut parsing = true;
@ -165,7 +165,7 @@ impl Args {
// This allows a file named “--arg” to be specified by passing in
// the pair “-- --arg”, without it getting matched as a flag that
// doesnt exist.
if !parsing {
if ! parsing {
frees.push(arg)
}
else if arg == "--" {
@ -183,8 +183,9 @@ impl Args {
let arg = self.lookup_long(before)?;
let flag = Flag::Long(arg.long);
match arg.takes_value {
Necessary(_)|Optional(_) => result_flags.push((flag, Some(after))),
Forbidden => return Err(ParseError::ForbiddenValue { flag })
TakesValue::Necessary(_) |
TakesValue::Optional(_) => result_flags.push((flag, Some(after))),
TakesValue::Forbidden => return Err(ParseError::ForbiddenValue { flag }),
}
}
@ -194,16 +195,18 @@ impl Args {
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) => {
TakesValue::Forbidden => {
result_flags.push((flag, None))
}
TakesValue::Necessary(values) => {
if let Some(next_arg) = inputs.next() {
result_flags.push((flag, Some(next_arg)));
}
else {
return Err(ParseError::NeedsValue { flag, values })
}
},
Optional(_) => {
}
TakesValue::Optional(_) => {
if let Some(next_arg) = inputs.next() {
result_flags.push((flag, Some(next_arg)));
}
@ -240,8 +243,13 @@ impl Args {
let arg = self.lookup_short(*byte)?;
let flag = Flag::Short(*byte);
match arg.takes_value {
Forbidden|Optional(_) => result_flags.push((flag, None)),
Necessary(values) => return Err(ParseError::NeedsValue { flag, values })
TakesValue::Forbidden |
TakesValue::Optional(_) => {
result_flags.push((flag, None));
}
TakesValue::Necessary(values) => {
return Err(ParseError::NeedsValue { flag, values });
}
}
}
@ -249,8 +257,13 @@ impl Args {
let arg = self.lookup_short(*arg_with_value)?;
let flag = Flag::Short(arg.short.unwrap());
match arg.takes_value {
Necessary(_)|Optional(_) => result_flags.push((flag, Some(after))),
Forbidden => return Err(ParseError::ForbiddenValue { flag })
TakesValue::Necessary(_) |
TakesValue::Optional(_) => {
result_flags.push((flag, Some(after)));
}
TakesValue::Forbidden => {
return Err(ParseError::ForbiddenValue { flag });
}
}
}
@ -271,8 +284,11 @@ impl Args {
let arg = self.lookup_short(*byte)?;
let flag = Flag::Short(*byte);
match arg.takes_value {
Forbidden => result_flags.push((flag, None)),
Necessary(values)|Optional(values) => {
TakesValue::Forbidden => {
result_flags.push((flag, None))
}
TakesValue::Necessary(values) |
TakesValue::Optional(values) => {
if index < bytes.len() - 1 {
let remnants = &bytes[index+1 ..];
result_flags.push((flag, Some(OsStr::from_bytes(remnants))));
@ -283,11 +299,13 @@ impl Args {
}
else {
match arg.takes_value {
Forbidden => unreachable!(),
Necessary(_) => {
TakesValue::Forbidden => {
unreachable!()
}
TakesValue::Necessary(_) => {
return Err(ParseError::NeedsValue { flag, values });
},
Optional(_) => {
}
TakesValue::Optional(_) => {
result_flags.push((flag, None));
}
}
@ -331,7 +349,7 @@ pub struct Matches<'args> {
pub flags: MatchedFlags<'args>,
/// All the strings that werent matched as arguments, as well as anything
/// after the special "--" string.
/// after the special “--” string.
pub frees: Vec<&'args OsStr>,
}
@ -355,8 +373,9 @@ impl<'a> MatchedFlags<'a> {
/// Whether the given argument was specified.
/// Returns `true` if it was, `false` if it wasnt, 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())
pub fn has(&self, arg: &'static Arg) -> Result<bool, OptionsError> {
self.has_where(|flag| flag.matches(arg))
.map(|flag| flag.is_some())
}
/// Returns the first found argument that satisfies the predicate, or
@ -364,7 +383,7 @@ impl<'a> MatchedFlags<'a> {
/// argument satisfy the predicate.
///
/// Youll have to test the resulting flag to see which argument it was.
pub fn has_where<P>(&self, predicate: P) -> Result<Option<&Flag>, Misfire>
pub fn has_where<P>(&self, predicate: P) -> Result<Option<&Flag>, OptionsError>
where P: Fn(&Flag) -> bool {
if self.is_strict() {
let all = self.flags.iter()
@ -372,7 +391,7 @@ impl<'a> MatchedFlags<'a> {
.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 { Err(OptionsError::Duplicate(all[0].0, all[1].0)) }
}
else {
let any = self.flags.iter().rev()
@ -389,7 +408,7 @@ impl<'a> MatchedFlags<'a> {
/// Returns the value of the given argument if it was specified, nothing
/// if it wasnt, and an error in strict mode if it was specified more
/// than once.
pub fn get(&self, arg: &'static Arg) -> Result<Option<&OsStr>, Misfire> {
pub fn get(&self, arg: &'static Arg) -> Result<Option<&OsStr>, OptionsError> {
self.get_where(|flag| flag.matches(arg))
}
@ -398,7 +417,7 @@ impl<'a> MatchedFlags<'a> {
/// multiple arguments matched the predicate.
///
/// Its not possible to tell which flag the value belonged to from this.
pub fn get_where<P>(&self, predicate: P) -> Result<Option<&OsStr>, Misfire>
pub fn get_where<P>(&self, predicate: P) -> Result<Option<&OsStr>, OptionsError>
where P: Fn(&Flag) -> bool {
if self.is_strict() {
let those = self.flags.iter()
@ -406,7 +425,7 @@ impl<'a> MatchedFlags<'a> {
.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 { Err(OptionsError::Duplicate(those[0].0, those[1].0)) }
}
else {
let found = self.flags.iter().rev()
@ -456,10 +475,17 @@ pub enum ParseError {
UnknownArgument { attempt: OsString },
}
// Its 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 its not really worth it.
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::NeedsValue { flag, values: None } => write!(f, "Flag {} needs a value", flag),
Self::NeedsValue { flag, values: Some(cs) } => write!(f, "Flag {} needs a value ({})", flag, Choices(cs)),
Self::ForbiddenValue { flag } => write!(f, "Flag {} cannot take a value", flag),
Self::UnknownShortArgument { attempt } => write!(f, "Unknown argument -{}", *attempt as char),
Self::UnknownArgument { attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()),
}
}
}
/// Splits a string on its `=` character, returning the two substrings on
@ -471,7 +497,7 @@ fn split_on_equals(input: &OsStr) -> Option<(&OsStr, &OsStr)> {
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 {
if ! before.is_empty() && after.len() >= 2 {
return Some((OsStr::from_bytes(before),
OsStr::from_bytes(&after[1..])))
}
@ -481,24 +507,16 @@ fn split_on_equals(input: &OsStr) -> Option<(&OsStr, &OsStr)> {
}
/// 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};
use super::split_on_equals;
use std::ffi::{OsStr, OsString};
macro_rules! test_split {
($name:ident: $input:expr => None) => {
#[test]
fn $name() {
assert_eq!(split_on_equals(&os($input)),
assert_eq!(split_on_equals(&OsString::from($input)),
None);
}
};
@ -506,8 +524,8 @@ mod split_test {
($name:ident: $input:expr => $before:expr, $after:expr) => {
#[test]
fn $name() {
assert_eq!(split_on_equals(&os($input)),
Some((&*os($before), &*os($after))));
assert_eq!(split_on_equals(&OsString::from($input)),
Some((OsStr::new($before), OsStr::new($after))));
}
};
}
@ -529,30 +547,24 @@ mod split_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();
let inputs: &[&'static str] = $inputs.as_ref();
let inputs = inputs.iter().map(OsStr::new);
// 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 frees: &[&'static str] = $frees.as_ref();
let frees = frees.iter().map(OsStr::new).collect();
let flags = <[_]>::into_vec(Box::new($flags));
let strictness = Strictness::UseLastArguments; // this isnt even used
let got = Args(TEST_ARGS).parse(inputs.iter(), strictness);
let expected = Ok(Matches { frees, flags: MatchedFlags { flags, strictness } });
let got = Args(TEST_ARGS).parse(inputs, strictness);
let flags = MatchedFlags { flags, strictness };
let expected = Ok(Matches { frees, flags });
assert_eq!(got, expected);
}
};
@ -562,10 +574,10 @@ mod parse_test {
fn $name() {
use self::ParseError::*;
let strictness = Strictness::UseLastArguments; // this isnt 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);
let inputs = $inputs.iter().map(OsStr::new);
let strictness = Strictness::UseLastArguments; // this isnt even used
let got = Args(TEST_ARGS).parse(inputs, strictness);
assert_eq!(got, Err($error));
}
};
@ -633,8 +645,8 @@ mod parse_test {
// Unknown args
test!(unknown_long: ["--quiet"] => error UnknownArgument { attempt: os("quiet") });
test!(unknown_long_eq: ["--quiet=shhh"] => error UnknownArgument { attempt: os("quiet") });
test!(unknown_long: ["--quiet"] => error UnknownArgument { attempt: OsString::from("quiet") });
test!(unknown_long_eq: ["--quiet=shhh"] => error UnknownArgument { attempt: OsString::from("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' });
@ -674,7 +686,7 @@ mod matches_test {
#[test]
fn only_count() {
let everything = os("everything");
let everything = OsString::from("everything");
let flags = MatchedFlags {
flags: vec![ (Flag::Short(b'c'), Some(&*everything)) ],
@ -686,8 +698,8 @@ mod matches_test {
#[test]
fn rightmost_count() {
let everything = os("everything");
let nothing = os("nothing");
let everything = OsString::from("everything");
let nothing = OsString::from("nothing");
let flags = MatchedFlags {
flags: vec![ (Flag::Short(b'c'), Some(&*everything)),
@ -702,6 +714,6 @@ mod matches_test {
fn no_count() {
let flags = MatchedFlags { flags: Vec::new(), strictness: Strictness::UseLastArguments };
assert!(!flags.has(&COUNT).unwrap());
assert_eq!(flags.has(&COUNT).unwrap(), false);
}
}

View File

@ -1,9 +1,9 @@
use ansi_term::Style;
use crate::fs::File;
use crate::options::{flags, Vars, Misfire};
use crate::options::{flags, Vars, OptionsError};
use crate::options::parser::MatchedFlags;
use crate::output::file_name::{FileStyle, Classify};
use crate::output::file_name::{Classify, FileStyle};
use crate::style::Colours;
@ -28,8 +28,8 @@ enum TerminalColours {
}
impl Default for TerminalColours {
fn default() -> TerminalColours {
TerminalColours::Automatic
fn default() -> Self {
Self::Automatic
}
}
@ -37,24 +37,23 @@ impl Default for TerminalColours {
impl TerminalColours {
/// Determine which terminal colour conditions to use.
fn deduce(matches: &MatchedFlags) -> Result<TerminalColours, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? {
Some(w) => w,
None => return Ok(TerminalColours::default()),
Some(w) => w,
None => return Ok(Self::default()),
};
if word == "always" {
Ok(TerminalColours::Always)
Ok(Self::Always)
}
else if word == "auto" || word == "automatic" {
Ok(TerminalColours::Automatic)
Ok(Self::Automatic)
}
else if word == "never" {
Ok(TerminalColours::Never)
Ok(Self::Never)
}
else {
Err(Misfire::BadArgument(&flags::COLOR, word.into()))
Err(OptionsError::BadArgument(&flags::COLOR, word.into()))
}
}
}
@ -77,10 +76,9 @@ pub struct Styles {
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>
#[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, OptionsError>
where TW: Fn() -> Option<usize>, V: Vars {
use self::TerminalColours::*;
use crate::info::filetype::FileExtensions;
use crate::output::file_name::NoFileColours;
@ -89,10 +87,13 @@ impl Styles {
// 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 {
if tc == TerminalColours::Never || (tc == TerminalColours::Automatic && widther().is_none()) {
let exts = Box::new(NoFileColours);
return Ok(Self {
colours: Colours::plain(),
style: FileStyle { classify, exts: Box::new(NoFileColours) },
style: FileStyle { classify, exts },
});
}
@ -111,18 +112,18 @@ impl Styles {
};
let style = FileStyle { classify, exts };
Ok(Styles { colours, style })
Ok(Self { colours, style })
}
}
/// Parse the environment variables into LS_COLORS pairs, putting file glob
/// 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
/// 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 log::warn;
use log::*;
use crate::options::vars;
use crate::style::LSColors;
@ -131,11 +132,16 @@ fn parse_color_vars<V: Vars>(vars: &V, colours: &mut Colours) -> (ExtensionMappi
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) {
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),
Ok(pat) => {
exts.add(pat, pair.to_style());
}
Err(e) => {
warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
}
}
}
});
@ -152,10 +158,14 @@ fn parse_color_vars<V: Vars>(vars: &V, colours: &mut Colours) -> (ExtensionMappi
}
LSColors(exa.as_ref()).each_pair(|pair| {
if !colours.set_ls(&pair) && !colours.set_exa(&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),
Ok(pat) => {
exts.add(pat, pair.to_style());
}
Err(e) => {
warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
}
}
};
});
@ -167,7 +177,7 @@ fn parse_color_vars<V: Vars>(vars: &V, colours: &mut Colours) -> (ExtensionMappi
#[derive(PartialEq, Debug, Default)]
struct ExtensionMappings {
mappings: Vec<(glob::Pattern, Style)>
mappings: Vec<(glob::Pattern, Style)>,
}
// Loop through backwards so that colours specified later in the list override
@ -176,9 +186,7 @@ struct ExtensionMappings {
use crate::output::file_name::FileColours;
impl FileColours for ExtensionMappings {
fn colour_file(&self, file: &File) -> Option<Style> {
self.mappings
.iter()
.rev()
self.mappings.iter().rev()
.find(|t| t.0.matches(&file.name))
.map (|t| t.1)
}
@ -186,7 +194,7 @@ impl FileColours for ExtensionMappings {
impl ExtensionMappings {
fn is_non_empty(&self) -> bool {
!self.mappings.is_empty()
! self.mappings.is_empty()
}
fn add(&mut self, pattern: glob::Pattern, style: Style) {
@ -195,18 +203,16 @@ impl ExtensionMappings {
}
impl Classify {
fn deduce(matches: &MatchedFlags) -> Result<Classify, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let flagged = matches.has(&flags::CLASSIFY)?;
Ok(if flagged { Classify::AddFileIndicators }
else { Classify::JustFilenames })
if flagged { Ok(Self::AddFileIndicators) }
else { Ok(Self::JustFilenames) }
}
}
#[cfg(test)]
mod terminal_test {
use super::*;
@ -254,8 +260,8 @@ mod terminal_test {
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!
test!(no_u_error: ["--color=upstream"]; Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("upstream"))); // the error is for --color
test!(u_error: ["--colour=lovers"]; Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one!
// Overriding
test!(overridden_1: ["--colour=auto", "--colour=never"]; Last => Ok(TerminalColours::Never));
@ -263,10 +269,10 @@ mod terminal_test {
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")));
test!(overridden_5: ["--colour=auto", "--colour=never"]; Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("colour")));
test!(overridden_6: ["--color=auto", "--colour=never"]; Complain => err OptionsError::Duplicate(Flag::Long("color"), Flag::Long("colour")));
test!(overridden_7: ["--colour=auto", "--color=never"]; Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("color")));
test!(overridden_8: ["--color=auto", "--color=never"]; Complain => err OptionsError::Duplicate(Flag::Long("color"), Flag::Long("color")));
}
@ -329,14 +335,13 @@ mod colour_test {
test!(scale_3: ["--color=always", "--colour-scale"], || None; Last => Ok(Colours::colourful(true)));
test!(scale_4: ["--color=always", ], || None; Last => Ok(Colours::colourful(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_5: ["--color=always", "--color-scale", "--colour-scale"], || None; Complain => err OptionsError::Duplicate(Flag::Long("color-scale"), Flag::Long("colour-scale")));
test!(scale_6: ["--color=always", "--color-scale", ], || None; Complain => Ok(Colours::colourful(true)));
test!(scale_7: ["--color=always", "--colour-scale"], || None; Complain => Ok(Colours::colourful(true)));
test!(scale_8: ["--color=always", ], || None; Complain => Ok(Colours::colourful(false)));
}
#[cfg(test)]
mod customs_test {
use std::ffi::OsString;
@ -408,11 +413,11 @@ mod customs_test {
fn get(&self, name: &'static str) -> Option<OsString> {
use crate::options::vars;
if name == vars::LS_COLORS && !self.ls.is_empty() {
OsString::from(self.ls.clone()).into()
if name == vars::LS_COLORS && ! self.ls.is_empty() {
Some(OsString::from(self.ls.clone()))
}
else if name == vars::EXA_COLORS && !self.exa.is_empty() {
OsString::from(self.exa.clone()).into()
else if name == vars::EXA_COLORS && ! self.exa.is_empty() {
Some(OsString::from(self.exa.clone()))
}
else {
None

View File

@ -15,10 +15,11 @@ 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 exas interface when colours are
/// enabled. This includes all the colours that LS_COLORS would recognise,
/// 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";
@ -39,7 +40,6 @@ pub static EXA_DEBUG: &str = "EXA_DEBUG";
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>;

View File

@ -8,7 +8,7 @@ use crate::options::flags;
use crate::options::parser::MatchedFlags;
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct VersionString;
// There were options here once, but there arent anymore!
@ -18,13 +18,13 @@ impl VersionString {
/// command-line arguments. This one works backwards from the other
/// deduce functions, returning Err if help needs to be shown.
///
/// Like --help, this doesnt bother checking for errors.
pub fn deduce(matches: &MatchedFlags) -> Result<(), VersionString> {
/// Like --help, this doesnt check for errors.
pub fn deduce(matches: &MatchedFlags) -> Option<Self> {
if matches.count(&flags::VERSION) > 0 {
Err(VersionString)
Some(Self)
}
else {
Ok(()) // no version needs to be shown
None
}
}
}
@ -38,19 +38,20 @@ impl fmt::Display for VersionString {
#[cfg(test)]
mod test {
use crate::options::Options;
use std::ffi::OsString;
use crate::options::{Options, OptionsResult};
use std::ffi::OsStr;
fn os(input: &'static str) -> OsString {
let mut os = OsString::new();
os.push(input);
os
#[test]
fn version() {
let args = vec![ OsStr::new("--version") ];
let opts = Options::parse(args, &None);
assert!(matches!(opts, OptionsResult::Version(_)));
}
#[test]
fn help() {
let args = [ os("--version") ];
let opts = Options::parse(&args, &None);
assert!(opts.is_err())
fn version_with_file() {
let args = vec![ OsStr::new("--version"), OsStr::new("me") ];
let opts = Options::parse(args, &None);
assert!(matches!(opts, OptionsResult::Version(_)));
}
}

View File

@ -1,24 +1,23 @@
use lazy_static::lazy_static;
use crate::options::{flags, Misfire, Vars};
use crate::fs::feature::xattr;
use crate::options::{flags, OptionsError, Vars};
use crate::options::parser::MatchedFlags;
use crate::output::{View, Mode, grid, details, lines};
use crate::output::grid_details::{self, RowThreshold};
use crate::output::table::{TimeTypes, Environment, SizeFormat, Columns, Options as TableOptions};
use crate::output::table::{TimeTypes, SizeFormat, Columns, Options as TableOptions};
use crate::output::time::TimeFormat;
use crate::fs::feature::xattr;
impl View {
/// Determine which view to use and all of that views arguments.
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<View, Misfire> {
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
use crate::options::style::Styles;
let mode = Mode::deduce(matches, vars)?;
let Styles { colours, style } = Styles::deduce(matches, vars, || *TERM_WIDTH)?;
Ok(View { mode, colours, style })
Ok(Self { mode, colours, style })
}
}
@ -26,15 +25,13 @@ impl View {
impl Mode {
/// Determine the mode from the command-line arguments.
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Mode, Misfire> {
use crate::options::misfire::Misfire::*;
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
let long = || {
if matches.has(&flags::ACROSS)? && !matches.has(&flags::GRID)? {
Err(Useless(&flags::ACROSS, true, &flags::LONG))
if matches.has(&flags::ACROSS)? && ! matches.has(&flags::GRID)? {
Err(OptionsError::Useless(&flags::ACROSS, true, &flags::LONG))
}
else if matches.has(&flags::ONE_LINE)? {
Err(Useless(&flags::ONE_LINE, true, &flags::LONG))
Err(OptionsError::Useless(&flags::ONE_LINE, true, &flags::LONG))
}
else {
Ok(details::Options {
@ -50,11 +47,11 @@ impl Mode {
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))
Err(OptionsError::Useless(&flags::ACROSS, true, &flags::ONE_LINE))
}
else {
let lines = lines::Options { icons: matches.has(&flags::ICONS)? };
Ok(Mode::Lines(lines))
Ok(Self::Lines(lines))
}
}
else if matches.has(&flags::TREE)? {
@ -65,7 +62,7 @@ impl Mode {
icons: matches.has(&flags::ICONS)?,
};
Ok(Mode::Details(details))
Ok(Self::Details(details))
}
else {
let grid = grid::Options {
@ -74,7 +71,7 @@ impl Mode {
icons: matches.has(&flags::ICONS)?,
};
Ok(Mode::Grid(grid))
Ok(Self::Grid(grid))
}
}
@ -89,14 +86,14 @@ impl Mode {
icons: matches.has(&flags::ICONS)?,
};
Ok(Mode::Details(details))
Ok(Self::Details(details))
}
else if matches.has(&flags::LONG)? {
let details = long()?;
Ok(Mode::Details(details))
Ok(Self::Details(details))
} else {
let lines = lines::Options { icons: matches.has(&flags::ICONS)?, };
Ok(Mode::Lines(lines))
Ok(Self::Lines(lines))
}
};
@ -104,16 +101,17 @@ impl Mode {
let details = long()?;
if matches.has(&flags::GRID)? {
let other_options_mode = other_options_scan()?;
if let Mode::Grid(grid) = other_options_mode {
if let Self::Grid(grid) = other_options_mode {
let row_threshold = RowThreshold::deduce(vars)?;
return Ok(Mode::GridDetails(grid_details::Options { grid, details, row_threshold }));
let opts = grid_details::Options { grid, details, row_threshold };
return Ok(Self::GridDetails(opts));
}
else {
return Ok(other_options_mode);
}
}
else {
return Ok(Mode::Details(details));
return Ok(Self::Details(details));
}
}
@ -123,17 +121,17 @@ impl Mode {
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));
return Err(OptionsError::Useless(*option, false, &flags::LONG));
}
}
if cfg!(feature="git") && matches.has(&flags::GIT)? {
return Err(Useless(&flags::GIT, false, &flags::LONG));
if cfg!(feature = "git") && matches.has(&flags::GIT)? {
return Err(OptionsError::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.
else if matches.has(&flags::LEVEL)? && ! matches.has(&flags::RECURSE)? && ! matches.has(&flags::TREE)? {
// TODO: Im 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));
return Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
}
}
@ -143,7 +141,7 @@ impl Mode {
/// The width of the terminal requested by the user.
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Copy, Clone)]
enum TerminalWidth {
/// The user requested this specific number of columns.
@ -161,28 +159,28 @@ impl TerminalWidth {
/// Determine a requested terminal width from the command-line arguments.
///
/// Returns an error if a requested width doesnt parse to an integer.
fn deduce<V: Vars>(vars: &V) -> Result<TerminalWidth, Misfire> {
fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
use crate::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)),
Ok(width) => Ok(Self::Set(width)),
Err(e) => Err(OptionsError::FailedParse(e)),
}
}
else if let Some(width) = *TERM_WIDTH {
Ok(TerminalWidth::Terminal(width))
Ok(Self::Terminal(width))
}
else {
Ok(TerminalWidth::Unset)
Ok(Self::Unset)
}
}
fn width(&self) -> Option<usize> {
match *self {
TerminalWidth::Set(width) |
TerminalWidth::Terminal(width) => Some(width),
TerminalWidth::Unset => None,
fn width(self) -> Option<usize> {
match self {
Self::Set(width) |
Self::Terminal(width) => Some(width),
Self::Unset => None,
}
}
}
@ -192,37 +190,36 @@ impl RowThreshold {
/// Determine whether to use a row threshold based on the given
/// environment variables.
fn deduce<V: Vars>(vars: &V) -> Result<RowThreshold, Misfire> {
fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
use crate::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)),
Ok(rows) => Ok(Self::MinimumRows(rows)),
Err(e) => Err(OptionsError::FailedParse(e)),
}
}
else {
Ok(RowThreshold::AlwaysGrid)
Ok(Self::AlwaysGrid)
}
}
}
impl TableOptions {
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, Misfire> {
let env = Environment::load_all();
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
let time_format = TimeFormat::deduce(matches, vars)?;
let size_format = SizeFormat::deduce(matches)?;
let columns = Columns::deduce(matches)?;
Ok(TableOptions { env, time_format, size_format, columns })
Ok(Self { time_format, size_format, columns })
}
}
impl Columns {
fn deduce(matches: &MatchedFlags) -> Result<Self, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let time_types = TimeTypes::deduce(matches)?;
let git = cfg!(feature="git") && matches.has(&flags::GIT)?;
let git = cfg!(feature = "git") && matches.has(&flags::GIT)?;
let blocks = matches.has(&flags::BLOCKS)?;
let group = matches.has(&flags::GROUP)?;
@ -230,11 +227,11 @@ impl Columns {
let links = matches.has(&flags::LINKS)?;
let octal = matches.has(&flags::OCTAL)?;
let permissions = !matches.has(&flags::NO_PERMISSIONS)?;
let filesize = !matches.has(&flags::NO_FILESIZE)?;
let user = !matches.has(&flags::NO_USER)?;
let permissions = ! matches.has(&flags::NO_PERMISSIONS)?;
let filesize = ! matches.has(&flags::NO_FILESIZE)?;
let user = ! matches.has(&flags::NO_USER)?;
Ok(Columns { time_types, git, octal, blocks, group, inode, links, permissions, filesize, user })
Ok(Self { time_types, git, octal, blocks, group, inode, links, permissions, filesize, user })
}
}
@ -249,13 +246,13 @@ 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: &MatchedFlags) -> Result<SizeFormat, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let flag = matches.has_where(|f| f.matches(&flags::BINARY) || f.matches(&flags::BYTES))?;
Ok(match flag {
Some(f) if f.matches(&flags::BINARY) => SizeFormat::BinaryBytes,
Some(f) if f.matches(&flags::BYTES) => SizeFormat::JustBytes,
_ => SizeFormat::DecimalBytes,
Some(f) if f.matches(&flags::BINARY) => Self::BinaryBytes,
Some(f) if f.matches(&flags::BYTES) => Self::JustBytes,
_ => Self::DecimalBytes,
})
}
}
@ -264,34 +261,34 @@ impl SizeFormat {
impl TimeFormat {
/// Determine how time should be formatted in timestamp columns.
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<TimeFormat, Misfire> {
pub use crate::output::time::{DefaultFormat, ISOFormat};
fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Self, OptionsError> {
let word = match matches.get(&flags::TIME_STYLE)? {
Some(w) => w.to_os_string(),
None => {
Some(w) => {
w.to_os_string()
}
None => {
use crate::options::vars;
match vars.get(vars::TIME_STYLE) {
Some(ref t) if !t.is_empty() => t.clone(),
_ => return Ok(TimeFormat::DefaultFormat(DefaultFormat::load()))
Some(ref t) if ! t.is_empty() => t.clone(),
_ => return Ok(Self::DefaultFormat)
}
},
};
if &word == "default" {
Ok(TimeFormat::DefaultFormat(DefaultFormat::load()))
Ok(Self::DefaultFormat)
}
else if &word == "iso" {
Ok(TimeFormat::ISOFormat(ISOFormat::load()))
Ok(Self::ISOFormat)
}
else if &word == "long-iso" {
Ok(TimeFormat::LongISO)
Ok(Self::LongISO)
}
else if &word == "full-iso" {
Ok(TimeFormat::FullISO)
Ok(Self::FullISO)
}
else {
Err(Misfire::BadArgument(&flags::TIME_STYLE, word))
Err(OptionsError::BadArgument(&flags::TIME_STYLE, word))
}
}
}
@ -309,7 +306,7 @@ impl TimeTypes {
/// Its 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: &MatchedFlags) -> Result<TimeTypes, Misfire> {
fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
let possible_word = matches.get(&flags::TIME)?;
let modified = matches.has(&flags::MODIFIED)?;
let changed = matches.has(&flags::CHANGED)?;
@ -319,41 +316,41 @@ impl TimeTypes {
let no_time = matches.has(&flags::NO_TIME)?;
let time_types = if no_time {
TimeTypes { modified: false, changed: false, accessed: false, created: false }
Self { modified: false, changed: false, accessed: false, created: false }
} else if let Some(word) = possible_word {
if modified {
return Err(Misfire::Useless(&flags::MODIFIED, true, &flags::TIME));
return Err(OptionsError::Useless(&flags::MODIFIED, true, &flags::TIME));
}
else if changed {
return Err(Misfire::Useless(&flags::CHANGED, true, &flags::TIME));
return Err(OptionsError::Useless(&flags::CHANGED, true, &flags::TIME));
}
else if accessed {
return Err(Misfire::Useless(&flags::ACCESSED, true, &flags::TIME));
return Err(OptionsError::Useless(&flags::ACCESSED, true, &flags::TIME));
}
else if created {
return Err(Misfire::Useless(&flags::CREATED, true, &flags::TIME));
return Err(OptionsError::Useless(&flags::CREATED, true, &flags::TIME));
}
else if word == "mod" || word == "modified" {
TimeTypes { modified: true, changed: false, accessed: false, created: false }
Self { modified: true, changed: false, accessed: false, created: false }
}
else if word == "ch" || word == "changed" {
TimeTypes { modified: false, changed: true, accessed: false, created: false }
Self { modified: false, changed: true, accessed: false, created: false }
}
else if word == "acc" || word == "accessed" {
TimeTypes { modified: false, changed: false, accessed: true, created: false }
Self { modified: false, changed: false, accessed: true, created: false }
}
else if word == "cr" || word == "created" {
TimeTypes { modified: false, changed: false, accessed: false, created: true }
Self { modified: false, changed: false, accessed: false, created: true }
}
else {
return Err(Misfire::BadArgument(&flags::TIME, word.into()));
return Err(OptionsError::BadArgument(&flags::TIME, word.into()));
}
}
else if modified || changed || accessed || created {
TimeTypes { modified, changed, accessed, created }
Self { modified, changed, accessed, created }
}
else {
TimeTypes::default()
Self::default()
};
Ok(time_types)
@ -375,7 +372,6 @@ lazy_static! {
}
#[cfg(test)]
mod test {
use super::*;
@ -475,10 +471,10 @@ mod test {
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")));
test!(both_5: SizeFormat <- ["--binary", "--binary"]; Complain => err OptionsError::Duplicate(Flag::Long("binary"), Flag::Long("binary")));
test!(both_6: SizeFormat <- ["--bytes", "--binary"]; Complain => err OptionsError::Duplicate(Flag::Long("bytes"), Flag::Long("binary")));
test!(both_7: SizeFormat <- ["--binary", "--bytes"]; Complain => err OptionsError::Duplicate(Flag::Long("binary"), Flag::Long("bytes")));
test!(both_8: SizeFormat <- ["--bytes", "--bytes"]; Complain => err OptionsError::Duplicate(Flag::Long("bytes"), Flag::Long("bytes")));
}
@ -489,23 +485,23 @@ mod test {
// implement PartialEq.
// Default behaviour
test!(empty: TimeFormat <- [], None; Both => like Ok(TimeFormat::DefaultFormat(_)));
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!(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!(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 OptionsError::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")));
test!(nevermore: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"], None; Complain => err OptionsError::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")));
test!(daily: TimeFormat <- ["--time-style=24-hour"], None; Both => err OptionsError::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.
@ -553,12 +549,12 @@ mod test {
// 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")));
test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err OptionsError::BadArgument(&flags::TIME, OsString::from("tea")));
test!(t_ea: TimeTypes <- ["-tea"]; Both => err OptionsError::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')));
test!(overridden_2: TimeTypes <- ["-tcr", "-tmod"]; Complain => err OptionsError::Duplicate(Flag::Short(b't'), Flag::Short(b't')));
}
@ -602,18 +598,18 @@ mod test {
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")]
#[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));
test!(just_header_2: Mode <- ["--header"], None; Complain => err OptionsError::Useless(&flags::HEADER, false, &flags::LONG));
test!(just_group_2: Mode <- ["--group"], None; Complain => err OptionsError::Useless(&flags::GROUP, false, &flags::LONG));
test!(just_inode_2: Mode <- ["--inode"], None; Complain => err OptionsError::Useless(&flags::INODE, false, &flags::LONG));
test!(just_links_2: Mode <- ["--links"], None; Complain => err OptionsError::Useless(&flags::LINKS, false, &flags::LONG));
test!(just_blocks_2: Mode <- ["--blocks"], None; Complain => err OptionsError::Useless(&flags::BLOCKS, false, &flags::LONG));
test!(just_binary_2: Mode <- ["--binary"], None; Complain => err OptionsError::Useless(&flags::BINARY, false, &flags::LONG));
test!(just_bytes_2: Mode <- ["--bytes"], None; Complain => err OptionsError::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));
#[cfg(feature = "git")]
test!(just_git_2: Mode <- ["--git"], None; Complain => err OptionsError::Useless(&flags::GIT, false, &flags::LONG));
}
}

View File

@ -42,7 +42,7 @@ impl TextCell {
pub fn paint(style: Style, text: String) -> Self {
let width = DisplayWidth::from(&*text);
TextCell {
Self {
contents: vec![ style.paint(text) ].into(),
width,
}
@ -54,7 +54,7 @@ impl TextCell {
pub fn paint_str(style: Style, text: &'static str) -> Self {
let width = DisplayWidth::from(text);
TextCell {
Self {
contents: vec![ style.paint(text) ].into(),
width,
}
@ -67,7 +67,7 @@ impl TextCell {
/// This is used in place of empty table cells, as it is easier to read
/// tabular data when there is *something* in each cell.
pub fn blank(style: Style) -> Self {
TextCell {
Self {
contents: vec![ style.paint("-") ].into(),
width: DisplayWidth::from(1),
}
@ -92,7 +92,7 @@ impl TextCell {
}
/// Adds all the contents of another `TextCell` to the end of this cell.
pub fn append(&mut self, other: TextCell) {
pub fn append(&mut self, other: Self) {
(*self.width) += *other.width;
self.contents.0.extend(other.contents.0);
}
@ -136,8 +136,8 @@ impl TextCell {
pub struct TextCellContents(Vec<ANSIString<'static>>);
impl From<Vec<ANSIString<'static>>> for TextCellContents {
fn from(strings: Vec<ANSIString<'static>>) -> TextCellContents {
TextCellContents(strings)
fn from(strings: Vec<ANSIString<'static>>) -> Self {
Self(strings)
}
}
@ -149,7 +149,7 @@ impl Deref for TextCellContents {
}
}
// No DerefMut implementation here -- it would be publicly accessible, and as
// No DerefMut implementation here it would be publicly accessible, and as
// the contents only get changed in this module, the mutators in the struct
// above can just access the value directly.
@ -165,7 +165,7 @@ impl TextCellContents {
/// counting the number of characters in each unformatted ANSI string.
pub fn width(&self) -> DisplayWidth {
self.0.iter()
.map(|anstr| DisplayWidth::from(anstr.deref()))
.map(|anstr| DisplayWidth::from(&**anstr))
.sum()
}
@ -188,7 +188,7 @@ impl TextCellContents {
/// when calculating widths for displaying tables in a terminal.
///
/// This type is used to ensure that the width, rather than the length, is
/// used when constructing a `TextCell` -- it's too easy to write something
/// used when constructing a `TextCell` — its too easy to write something
/// like `file_name.len()` and assume it will work!
///
/// It has `From` impls that convert an input string or fixed with to values
@ -197,14 +197,14 @@ impl TextCellContents {
pub struct DisplayWidth(usize);
impl<'a> From<&'a str> for DisplayWidth {
fn from(input: &'a str) -> DisplayWidth {
DisplayWidth(UnicodeWidthStr::width(input))
fn from(input: &'a str) -> Self {
Self(UnicodeWidthStr::width(input))
}
}
impl From<usize> for DisplayWidth {
fn from(width: usize) -> DisplayWidth {
DisplayWidth(width)
fn from(width: usize) -> Self {
Self(width)
}
}
@ -223,24 +223,26 @@ impl DerefMut for DisplayWidth {
}
impl Add for DisplayWidth {
type Output = DisplayWidth;
type Output = Self;
fn add(self, rhs: DisplayWidth) -> Self::Output {
DisplayWidth(self.0 + rhs.0)
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Add<usize> for DisplayWidth {
type Output = DisplayWidth;
type Output = Self;
fn add(self, rhs: usize) -> Self::Output {
DisplayWidth(self.0 + rhs)
Self(self.0 + rhs)
}
}
impl Sum for DisplayWidth {
fn sum<I>(iter: I) -> Self where I: Iterator<Item=Self> {
iter.fold(DisplayWidth(0), Add::add)
fn sum<I>(iter: I) -> Self
where I: Iterator<Item = Self>
{
iter.fold(Self(0), Add::add)
}
}

View File

@ -1,6 +1,6 @@
//! The **Details** output view displays each file as a row in a table.
//!
//! It's used in the following situations:
//! Its used in the following situations:
//!
//! - Most commonly, when using the `--long` command-line argument to display the
//! details of each file, which requires using a table view to hold all the data;
@ -60,26 +60,25 @@
//! can be displayed, in order to make sure that every column is wide enough.
use std::io::{Write, Error as IOError, Result as IOResult};
use std::io::{self, Write};
use std::mem::MaybeUninit;
use std::path::PathBuf;
use std::vec::IntoIter as VecIntoIter;
use ansi_term::{ANSIGenericString, Style};
use scoped_threadpool::Pool;
use crate::fs::{Dir, File};
use crate::fs::dir_action::RecurseOptions;
use crate::fs::filter::FileFilter;
use crate::fs::feature::git::GitCache;
use crate::fs::feature::xattr::{Attribute, FileAttributes};
use crate::fs::filter::FileFilter;
use crate::style::Colours;
use crate::output::cell::TextCell;
use crate::output::tree::{TreeTrunk, TreeParams, TreeDepth};
use crate::output::icons::painted_icon;
use crate::output::file_name::FileStyle;
use crate::output::table::{Table, Options as TableOptions, Row as TableRow};
use crate::output::icons::painted_icon;
use scoped_threadpool::Pool;
use crate::output::tree::{TreeTrunk, TreeParams, TreeDepth};
/// With the **Details** view, the output gets formatted into columns, with
@ -93,7 +92,7 @@ use scoped_threadpool::Pool;
///
/// Almost all the heavy lifting is done in a Table object, which handles the
/// columns for each row.
#[derive(Debug)]
#[derive(PartialEq, Debug)]
pub struct Options {
/// Options specific to drawing a table.
@ -105,15 +104,14 @@ pub struct Options {
/// Whether to show a header line or not.
pub header: bool,
/// Whether to show each file's extended attributes.
/// Whether to show each files extended attributes.
pub xattr: bool,
/// Enables --icons mode
/// Whether icons mode is enabled.
pub icons: bool,
}
pub struct Render<'a> {
pub dir: Option<&'a Dir>,
pub files: Vec<File<'a>>,
@ -131,13 +129,15 @@ pub struct Render<'a> {
/// Whether we are skipping Git-ignored files.
pub git_ignoring: bool,
pub git: Option<&'a GitCache>,
}
struct Egg<'a> {
table_row: Option<TableRow>,
xattrs: Vec<Attribute>,
errors: Vec<(IOError, Option<PathBuf>)>,
errors: Vec<(io::Error, Option<PathBuf>)>,
dir: Option<Dir>,
file: &'a File<'a>,
icon: Option<String>,
@ -151,18 +151,18 @@ impl<'a> AsRef<File<'a>> for Egg<'a> {
impl<'a> Render<'a> {
pub fn render<W: Write>(self, mut git: Option<&'a GitCache>, w: &mut W) -> IOResult<()> {
pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
let mut pool = Pool::new(num_cpus::get() as u32);
let mut rows = Vec::new();
if let Some(ref table) = self.opts.table {
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 },
match (self.git, self.dir) {
(Some(g), Some(d)) => if ! g.has_anything_for(&d.path) { self.git = None },
(Some(g), None) => if ! self.files.iter().any(|f| g.has_anything_for(&f.path)) { self.git = None },
(None, _) => {/* Keep Git how it is */},
}
let mut table = Table::new(&table, git, &self.colours);
let mut table = Table::new(table, self.git, self.colours);
if self.opts.header {
let header = table.header_row();
@ -173,14 +173,14 @@ impl<'a> Render<'a> {
// This is weird, but I cant 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 pool, &mut table, &mut rows, &self.files, git, TreeDepth::root());
self.add_files_to_table(&mut pool, &mut table, &mut rows, &self.files, TreeDepth::root());
for row in self.iterate_with_table(table.unwrap(), rows) {
writeln!(w, "{}", row.strings())?
}
}
else {
self.add_files_to_table(&mut pool, &mut None, &mut rows, &self.files, git, TreeDepth::root());
self.add_files_to_table(&mut pool, &mut None, &mut rows, &self.files, TreeDepth::root());
for row in self.iterate(rows) {
writeln!(w, "{}", row.strings())?
@ -192,9 +192,9 @@ 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, 'ig>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], git: Option<&'ig GitCache>, depth: TreeDepth) {
fn add_files_to_table<'dir>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], depth: TreeDepth) {
use std::sync::{Arc, Mutex};
use log::error;
use log::*;
use crate::fs::feature::xattr;
let mut file_eggs = (0..src.len()).map(|_| MaybeUninit::uninit()).collect::<Vec<_>>();
@ -248,26 +248,26 @@ impl<'a> Render<'a> {
}
}
let table_row = table.as_ref().map(|t| t.row_for_file(&file, !xattrs.is_empty()));
let table_row = table.as_ref()
.map(|t| t.row_for_file(file, ! xattrs.is_empty()));
if !self.opts.xattr {
if ! self.opts.xattr {
xattrs.clear();
}
let mut dir = None;
if let Some(r) = self.recurse {
if file.is_directory() && r.tree && !r.is_too_deep(depth.0) {
if file.is_directory() && r.tree && ! r.is_too_deep(depth.0) {
match file.to_dir() {
Ok(d) => { dir = Some(d); },
Err(e) => { errors.push((e, None)) },
Ok(d) => { dir = Some(d); },
Err(e) => { errors.push((e, None)) },
}
}
};
let icon = if self.opts.icons {
Some(painted_icon(&file, &self.style))
} else { None };
let icon = if self.opts.icons { Some(painted_icon(file, self.style)) }
else { None };
let egg = Egg { table_row, xattrs, errors, dir, file, icon };
unsafe { std::ptr::write(file_eggs.lock().unwrap()[idx].as_mut_ptr(), egg) }
@ -283,7 +283,7 @@ impl<'a> Render<'a> {
let mut files = Vec::new();
let mut errors = egg.errors;
if let (Some(ref mut t), Some(ref row)) = (table.as_mut(), egg.table_row.as_ref()) {
if let (Some(ref mut t), Some(row)) = (table.as_mut(), egg.table_row.as_ref()) {
t.add_widths(row);
}
@ -291,10 +291,12 @@ impl<'a> Render<'a> {
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 style = self.style.for_file(egg.file, self.colours)
.with_link_paths()
.paint()
.promote();
name_cell.append(style);
let row = Row {
@ -306,16 +308,20 @@ impl<'a> Render<'a> {
rows.push(row);
if let Some(ref dir) = egg.dir {
for file_to_add in dir.files(self.filter.dot_filter, git, self.git_ignoring) {
for file_to_add in dir.files(self.filter.dot_filter, self.git, self.git_ignoring) {
match file_to_add {
Ok(f) => files.push(f),
Err((path, e)) => errors.push((e, Some(path)))
Ok(f) => {
files.push(f);
}
Err((path, e)) => {
errors.push((e, Some(path)));
}
}
}
self.filter.filter_child_files(&mut files);
if !files.is_empty() {
if ! files.is_empty() {
for xattr in egg.xattrs {
rows.push(self.render_xattr(&xattr, TreeParams::new(depth.deeper(), false)));
}
@ -324,19 +330,23 @@ impl<'a> Render<'a> {
rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), false), path));
}
self.add_files_to_table(pool, table, rows, &files, git, depth.deeper());
self.add_files_to_table(pool, table, rows, &files, 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)));
let params = TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1);
let r = self.render_xattr(&xattr, params);
rows.push(r);
}
let count = errors.len();
for (index, (error, path)) in errors.into_iter().enumerate() {
rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), index == count - 1), path));
let params = TreeParams::new(depth.deeper(), index == count - 1);
let r = self.render_error(&error, params, path);
rows.push(r);
}
}
}
@ -349,7 +359,7 @@ impl<'a> Render<'a> {
}
}
fn render_error(&self, error: &IOError, tree: TreeParams, path: Option<PathBuf>) -> Row {
fn render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row {
use crate::output::file_name::Colours;
let error_message = match path {
@ -396,13 +406,13 @@ pub struct Row {
/// Vector of cells to display.
///
/// Most of the rows will be used to display files' metadata, so this will
/// Most of the rows will be used to display files metadata, so this will
/// almost always be `Some`, containing a vector of cells. It will only be
/// `None` for a row displaying an attribute or error, neither of which
/// have cells.
pub cells: Option<TableRow>,
/// This file's name, in coloured output. The name is treated separately
/// This files name, in coloured output. The name is treated separately
/// from the other cells, as it never requires padding.
pub name: TextCell,
@ -441,7 +451,7 @@ impl<'a> Iterator for TableIter<'a> {
// If any tree characters have been printed, then add an extra
// space, which makes the output look much better.
if !row.tree.is_at_root() {
if ! row.tree.is_at_root() {
cell.add_spaces(1);
}
@ -471,7 +481,7 @@ impl Iterator for Iter {
// If any tree characters have been printed, then add an extra
// space, which makes the output look much better.
if !row.tree.is_at_root() {
if ! row.tree.is_at_root() {
cell.add_spaces(1);
}

View File

@ -4,22 +4,23 @@ use ansi_term::{ANSIString, Style};
pub fn escape<'a>(string: String, bits: &mut Vec<ANSIString<'a>>, good: Style, bad: Style) {
if string.chars().all(|c| c >= 0x20 as char && c != 0x7f as char) {
bits.push(good.paint(string));
return;
}
else {
for c in string.chars() {
// The `escape_default` method on `char` is *almost* what we want here, but
// it still escapes non-ASCII UTF-8 characters, which are still printable.
if c >= 0x20 as char && c != 0x7f as char {
// TODO: This allocates way too much,
// hence the `all` check above.
let mut s = String::new();
s.push(c);
bits.push(good.paint(s));
} else {
let s = c.escape_default().collect::<String>();
bits.push(bad.paint(s));
}
for c in string.chars() {
// The `escape_default` method on `char` is *almost* what we want here, but
// it still escapes non-ASCII UTF-8 characters, which are still printable.
if c >= 0x20 as char && c != 0x7f as char {
// TODO: This allocates way too much,
// hence the `all` check above.
let mut s = String::new();
s.push(c);
bits.push(good.paint(s));
}
else {
let s = c.escape_default().collect::<String>();
bits.push(bad.paint(s));
}
}
}

View File

@ -1,10 +1,12 @@
use std::fmt::Debug;
use std::marker::Sync;
use std::path::Path;
use ansi_term::{ANSIString, Style};
use crate::fs::{File, FileTarget};
use crate::output::escape;
use crate::output::cell::TextCellContents;
use crate::output::escape;
use crate::output::render::FiletypeColours;
@ -25,7 +27,8 @@ impl FileStyle {
/// with the remaining arguments.
pub fn for_file<'a, 'dir, C: Colours>(&'a self, file: &'a File<'dir>, colours: &'a C) -> FileName<'a, 'dir, C> {
FileName {
file, colours,
file,
colours,
link_style: LinkStyle::JustFilenames,
classify: self.classify,
exts: &*self.exts,
@ -65,13 +68,12 @@ pub enum Classify {
}
impl Default for Classify {
fn default() -> Classify {
Classify::JustFilenames
fn default() -> Self {
Self::JustFilenames
}
}
/// 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, C: Colours+'a> {
@ -120,9 +122,9 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
}
}
if !self.file.name.is_empty() {
if ! self.file.name.is_empty() {
// The “missing file” colour seems like it should be used here,
// but its not! In a grid view, where there's no space to display
// but its not! In a grid view, where theres 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
@ -133,8 +135,8 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
}
if let (LinkStyle::FullLinkPaths, Some(target)) = (self.link_style, self.target.as_ref()) {
match *target {
FileTarget::Ok(ref target) => {
match target {
FileTarget::Ok(target) => {
bits.push(Style::default().paint(" "));
bits.push(self.colours.normal_arrow().paint("->"));
bits.push(Style::default().paint(" "));
@ -143,7 +145,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
self.add_parent_bits(&mut bits, parent);
}
if !target.name.is_empty() {
if ! target.name.is_empty() {
let target = FileName {
file: target,
colours: self.colours,
@ -157,18 +159,24 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
bits.push(bit);
}
}
},
}
FileTarget::Broken(ref broken_path) => {
FileTarget::Broken(broken_path) => {
bits.push(Style::default().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.broken_control_char());
},
escape(
broken_path.display().to_string(),
&mut bits,
self.colours.broken_filename(),
self.colours.broken_control_char(),
);
}
FileTarget::Err(_) => {
// Do nothing -- the error gets displayed on the next line
},
// Do nothing the error gets displayed on the next line
}
}
}
else if let Classify::AddFileIndicators = self.classify {
@ -190,7 +198,12 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
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());
escape(
parent.to_string_lossy().to_string(),
bits,
self.colours.symlink_path(),
self.colours.control_char(),
);
bits.push(self.colours.symlink_path().paint("/"));
}
}
@ -201,15 +214,20 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
fn classify_char(&self) -> Option<&'static str> {
if self.file.is_executable_file() {
Some("*")
} else if self.file.is_directory() {
}
else if self.file.is_directory() {
Some("/")
} else if self.file.is_pipe() {
}
else if self.file.is_pipe() {
Some("|")
} else if self.file.is_link() {
}
else if self.file.is_link() {
Some("@")
} else if self.file.is_socket() {
}
else if self.file.is_socket() {
Some("=")
} else {
}
else {
None
}
}
@ -228,13 +246,20 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
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
/// depending on which “type” of file it appears to be either from the
/// class on the filesystem or from its name. (Or the broken link colour,
/// if theres nowhere else for that fact to be shown.)
pub fn style(&self) -> Style {
@ -260,7 +285,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
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(),
f if ! f.is_file() => self.colours.special(),
_ => return None,
})
}
@ -298,9 +323,7 @@ pub trait Colours: FiletypeColours {
// needs Debug because FileStyle derives it
use std::fmt::Debug;
use std::marker::Sync;
pub trait FileColours: Debug+Sync {
pub trait FileColours: Debug + Sync {
fn colour_file(&self, file: &File) -> Option<Style>;
}
@ -308,7 +331,9 @@ pub trait FileColours: Debug+Sync {
#[derive(PartialEq, Debug)]
pub struct NoFileColours;
impl FileColours for NoFileColours {
fn colour_file(&self, _file: &File) -> Option<Style> { None }
fn colour_file(&self, _file: &File) -> Option<Style> {
None
}
}
// When getting the colour of a file from a *pair* of colourisers, try the
@ -316,8 +341,11 @@ impl FileColours for NoFileColours {
// 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 {
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))
self.0.colour_file(file)
.or_else(|| self.1.colour_file(file))
}
}

View File

@ -1,12 +1,12 @@
use std::io::{Write, Result as IOResult};
use std::io::{self, Write};
use term_grid as tg;
use crate::fs::File;
use crate::style::Colours;
use crate::output::cell::DisplayWidth;
use crate::output::file_name::FileStyle;
use crate::output::icons::painted_icon;
use crate::output::cell::DisplayWidth;
use crate::style::Colours;
#[derive(PartialEq, Debug, Copy, Clone)]
@ -32,7 +32,7 @@ pub struct Render<'a> {
}
impl<'a> Render<'a> {
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
pub fn render<W: Write>(&self, w: &mut W) -> io::Result<()> {
let mut grid = tg::Grid::new(tg::GridOptions {
direction: self.opts.direction(),
filling: tg::Filling::Spaces(2),
@ -41,16 +41,16 @@ impl<'a> Render<'a> {
grid.reserve(self.files.len());
for file in &self.files {
let icon = if self.opts.icons { Some(painted_icon(&file, &self.style)) } else { None };
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 = if self.opts.icons {
DisplayWidth::from(2) + filename.width()
} else {
filename.width()
};
let width = if self.opts.icons { DisplayWidth::from(2) + filename.width() }
else { filename.width() };
grid.add(tg::Cell {
contents: format!("{icon}{filename}", icon=&icon.unwrap_or_default(), filename=filename.strings().to_string()),
contents: format!("{}{}", &icon.unwrap_or_default(), filename.strings()),
width: *width,
});
}
@ -64,11 +64,13 @@ impl<'a> Render<'a> {
// displays full link paths.
for file in &self.files {
if self.opts.icons {
write!(w, "{}", painted_icon(&file, &self.style))?;
write!(w, "{}", painted_icon(file, self.style))?;
}
let name_cell = self.style.for_file(file, self.colours).paint();
writeln!(w, "{}", name_cell.strings())?;
}
Ok(())
}
}

View File

@ -1,6 +1,6 @@
//! The grid-details view lists several details views side-by-side.
use std::io::{Write, Result as IOResult};
use std::io::{self, Write};
use ansi_term::{ANSIGenericString, ANSIStrings};
use term_grid as grid;
@ -9,18 +9,17 @@ use crate::fs::{Dir, File};
use crate::fs::feature::git::GitCache;
use crate::fs::feature::xattr::FileAttributes;
use crate::fs::filter::FileFilter;
use crate::style::Colours;
use crate::output::cell::TextCell;
use crate::output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
use crate::output::grid::Options as GridOptions;
use crate::output::file_name::FileStyle;
use crate::output::grid::Options as GridOptions;
use crate::output::icons::painted_icon;
use crate::output::table::{Table, Row as TableRow, Options as TableOptions};
use crate::output::tree::{TreeParams, TreeDepth};
use crate::output::icons::painted_icon;
use crate::style::Colours;
#[derive(Debug)]
#[derive(PartialEq, Debug)]
pub struct Options {
pub grid: GridOptions,
pub details: DetailsOptions,
@ -34,7 +33,7 @@ pub struct Options {
/// small directory of four files in four columns, the files just look spaced
/// out and its harder to see whats going on. So it can be enabled just for
/// larger directory listings.
#[derive(Copy, Clone, Debug, PartialEq)]
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum RowThreshold {
/// Only use grid-details view if it would result in at least this many
@ -79,26 +78,29 @@ pub struct Render<'a> {
/// Whether we are skipping Git-ignored files.
pub git_ignoring: bool,
pub git: Option<&'a GitCache>,
}
impl<'a> Render<'a> {
/// Create a temporary Details render that gets used for the columns of
/// the grid-details render that's being generated.
/// the grid-details render thats 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 columns table, not all of them.
pub fn details(&self) -> DetailsRender<'a> {
DetailsRender {
dir: self.dir,
files: Vec::new(),
colours: self.colours,
style: self.style,
opts: self.details,
recurse: None,
filter: self.filter,
git_ignoring: self.git_ignoring,
dir: self.dir,
files: Vec::new(),
colours: self.colours,
style: self.style,
opts: self.details,
recurse: None,
filter: self.filter,
git_ignoring: self.git_ignoring,
git: self.git,
}
}
@ -106,60 +108,62 @@ impl<'a> Render<'a> {
/// 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,
git_ignoring: self.git_ignoring,
dir: self.dir,
files: self.files,
colours: self.colours,
style: self.style,
opts: self.details,
recurse: None,
filter: self.filter,
git_ignoring: self.git_ignoring,
git: self.git,
}
}
// This doesnt 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) {
pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
if let Some((grid, width)) = self.find_fitting_grid() {
write!(w, "{}", grid.fit_into_columns(width))
}
else {
self.give_up().render(git, w)
self.give_up().render(w)
}
}
pub fn find_fitting_grid(&self, git: Option<&GitCache>) -> Option<(grid::Grid, grid::Width)> {
pub fn find_fitting_grid(&mut self) -> Option<(grid::Grid, grid::Width)> {
let options = self.details.table.as_ref().expect("Details table options not given!");
let drender = self.details();
let (first_table, _) = self.make_table(options, git, &drender);
let (first_table, _) = self.make_table(options, &drender);
let rows = self.files.iter()
.map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
.collect::<Vec<TableRow>>();
.collect::<Vec<_>>();
let file_names = self.files.iter()
.map(|file| {
if self.details.icons {
let mut icon_cell = TextCell::default();
icon_cell.push(ANSIGenericString::from(painted_icon(&file, &self.style)), 2);
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 {
}
else {
self.style.for_file(file, self.colours).paint().promote()
}
})
.collect::<Vec<TextCell>>();
.collect::<Vec<_>>();
let mut last_working_table = self.make_grid(1, options, git, &file_names, rows.clone(), &drender);
let mut last_working_table = self.make_grid(1, options, &file_names, rows.clone(), &drender);
// If we cant 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 grid = self.make_grid(column_count, options, &file_names, rows.clone(), &drender);
let the_grid_fits = {
let d = grid.fit_into_columns(column_count);
@ -186,14 +190,14 @@ impl<'a> Render<'a> {
None
}
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 },
fn make_table(&mut self, options: &'a TableOptions, drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
match (self.git, self.dir) {
(Some(g), Some(d)) => if ! g.has_anything_for(&d.path) { self.git = None },
(Some(g), None) => if ! self.files.iter().any(|f| g.has_anything_for(&f.path)) { self.git = None },
(None, _) => {/* Keep Git how it is */},
}
let mut table = Table::new(options, git, self.colours);
let mut table = Table::new(options, self.git, self.colours);
let mut rows = Vec::new();
if self.details.header {
@ -205,11 +209,10 @@ impl<'a> Render<'a> {
(table, rows)
}
fn make_grid(&'a self, column_count: usize, options: &'a TableOptions, git: Option<&GitCache>, file_names: &[TextCell], rows: Vec<TableRow>, drender: &DetailsRender) -> grid::Grid {
fn make_grid(&mut self, column_count: usize, options: &'a TableOptions, 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, git, drender));
tables.push(self.make_table(options, drender));
}
let mut num_cells = rows.len();
@ -234,17 +237,19 @@ impl<'a> Render<'a> {
rows.push(details_row);
}
let columns: Vec<_> = tables.into_iter().map(|(table, details_rows)| {
drender.iterate_with_table(table, details_rows).collect::<Vec<_>>()
}).collect();
let columns = tables
.into_iter()
.map(|(table, details_rows)| {
drender.iterate_with_table(table, details_rows)
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let direction = if self.grid.across { grid::Direction::LeftToRight }
else { grid::Direction::TopToBottom };
let mut grid = grid::Grid::new(grid::GridOptions {
direction,
filling: grid::Filling::Spaces(4),
});
let filling = grid::Filling::Spaces(4);
let mut grid = grid::Grid::new(grid::GridOptions { direction, filling });
if self.grid.across {
for row in 0 .. height {
@ -280,14 +285,18 @@ impl<'a> Render<'a> {
fn divide_rounding_up(a: usize, b: usize) -> usize {
let mut result = a / b;
if a % b != 0 { result += 1; }
if a % b != 0 {
result += 1;
}
result
}
fn file_has_xattrs(file: &File) -> bool {
match file.path.attributes() {
Ok(attrs) => !attrs.is_empty(),
Err(_) => false,
Ok(attrs) => ! attrs.is_empty(),
Err(_) => false,
}
}

View File

@ -4,10 +4,13 @@ use crate::fs::File;
use crate::info::filetype::FileExtensions;
use crate::output::file_name::FileStyle;
pub trait FileIcon {
fn icon_file(&self, file: &File) -> Option<char>;
}
#[derive(Copy, Clone)]
pub enum Icons {
Audio,
Image,
@ -15,37 +18,50 @@ pub enum Icons {
}
impl Icons {
pub fn value(&self) -> char {
match *self {
Icons::Audio => '\u{f001}',
Icons::Image => '\u{f1c5}',
Icons::Video => '\u{f03d}',
pub fn value(self) -> char {
match self {
Self::Audio => '\u{f001}',
Self::Image => '\u{f1c5}',
Self::Video => '\u{f03d}',
}
}
}
pub fn painted_icon(file: &File, style: &FileStyle) -> String {
let file_icon = icon(&file).to_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(),
.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()
}
});
}
else {
c.paint(file_icon).to_string()
}
});
format!("{} ", painted)
}
fn icon(file: &File) -> char {
let extensions = Box::new(FileExtensions);
if file.points_to_directory() { '\u{f115}' }
else if let Some(icon) = extensions.icon_file(file) { icon }
if file.points_to_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}',
@ -188,7 +204,8 @@ fn icon(file: &File) -> char {
"nix" => '\u{f313}',
_ => '\u{f016}'
}
} else {
}
else {
'\u{f016}'
}
}

View File

@ -1,16 +1,17 @@
use std::io::{Write, Result as IOResult};
use std::io::{self, Write};
use ansi_term::{ANSIStrings, ANSIGenericString};
use crate::fs::File;
use crate::output::file_name::{FileName, FileStyle};
use crate::style::Colours;
use crate::output::icons::painted_icon;
use crate::output::cell::TextCell;
use crate::output::file_name::{FileName, FileStyle};
use crate::output::icons::painted_icon;
use crate::style::Colours;
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Options {
pub icons: bool
pub icons: bool,
}
/// The lines view literally just displays each file, line-by-line.
@ -22,17 +23,18 @@ pub struct Render<'a> {
}
impl<'a> Render<'a> {
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
pub fn render<W: Write>(&self, w: &mut W) -> io::Result<()> {
for file in &self.files {
let name_cell = self.render_file(file).paint();
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);
let icon = painted_icon(file, self.style);
cell.push(ANSIGenericString::from(icon), 2);
cell.append(name_cell.promote());
writeln!(w, "{}", ANSIStrings(&cell))?;
} else {
}
else {
writeln!(w, "{}", ANSIStrings(&name_cell))?;
}
}

View File

@ -6,8 +6,8 @@ pub use self::escape::escape;
pub mod details;
pub mod file_name;
pub mod grid_details;
pub mod grid;
pub mod grid_details;
pub mod icons;
pub mod lines;
pub mod render;
@ -29,7 +29,7 @@ pub struct View {
/// The **mode** is the “type” of output.
#[derive(Debug)]
#[derive(PartialEq, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Mode {
Grid(grid::Options),

View File

@ -1,14 +1,14 @@
use ansi_term::Style;
use crate::output::cell::TextCell;
use crate::fs::fields as f;
use crate::output::cell::TextCell;
impl f::Blocks {
pub fn render<C: Colours>(&self, colours: &C) -> TextCell {
match *self {
f::Blocks::Some(ref blk) => TextCell::paint(colours.block_count(), blk.to_string()),
f::Blocks::None => TextCell::blank(colours.no_blocks()),
match self {
Self::Some(blk) => TextCell::paint(colours.block_count(), blk.to_string()),
Self::None => TextCell::blank(colours.no_blocks()),
}
}
}

View File

@ -4,16 +4,16 @@ use crate::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 fn render<C: Colours>(self, colours: &C) -> ANSIString<'static> {
match self {
Self::File => colours.normal().paint("."),
Self::Directory => colours.directory().paint("d"),
Self::Pipe => colours.pipe().paint("|"),
Self::Link => colours.symlink().paint("l"),
Self::BlockDevice => colours.block_device().paint("b"),
Self::CharDevice => colours.char_device().paint("c"),
Self::Socket => colours.socket().paint("s"),
Self::Special => colours.special().paint("?"),
}
}
}

View File

@ -5,7 +5,7 @@ use crate::fs::fields as f;
impl f::Git {
pub fn render(&self, colours: &dyn Colours) -> TextCell {
pub fn render(self, colours: &dyn Colours) -> TextCell {
TextCell {
width: DisplayWidth::from(2),
contents: vec![
@ -18,16 +18,16 @@ impl f::Git {
impl f::GitStatus {
fn render(&self, colours: &dyn Colours) -> ANSIString<'static> {
match *self {
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"),
f::GitStatus::Conflicted => colours.conflicted().paint("U"),
fn render(self, colours: &dyn Colours) -> ANSIString<'static> {
match self {
Self::NotModified => colours.not_modified().paint("-"),
Self::New => colours.new().paint("N"),
Self::Modified => colours.modified().paint("M"),
Self::Deleted => colours.deleted().paint("D"),
Self::Renamed => colours.renamed().paint("R"),
Self::TypeChange => colours.type_change().paint("T"),
Self::Ignored => colours.ignored().paint("I"),
Self::Conflicted => colours.conflicted().paint("U"),
}
}
}
@ -65,7 +65,7 @@ pub mod test {
fn renamed(&self) -> Style { Fixed(94).normal() }
fn type_change(&self) -> Style { Fixed(95).normal() }
fn ignored(&self) -> Style { Fixed(96).normal() }
fn conflicted(&self) -> Style { Fixed(93).normal() }
fn conflicted(&self) -> Style { Fixed(97).normal() }
}

View File

@ -6,20 +6,22 @@ use crate::output::cell::TextCell;
impl f::Group {
pub fn render<C: Colours, U: Users+Groups>(&self, colours: &C, 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.not_yours();
let group = match users.get_group_by_gid(self.0) {
Some(g) => (*g).clone(),
None => return TextCell::paint(style, self.0.to_string()),
Some(g) => (*g).clone(),
None => return TextCell::paint(style, self.0.to_string()),
};
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().iter().any(|u| u == current_user.name()) {
|| group.members().iter().any(|u| u == current_user.name())
{
style = colours.yours();
}
}

View File

@ -1,11 +1,11 @@
use ansi_term::Style;
use crate::output::cell::TextCell;
use crate::fs::fields as f;
use crate::output::cell::TextCell;
impl f::Inode {
pub fn render(&self, style: Style) -> TextCell {
pub fn render(self, style: Style) -> TextCell {
TextCell::paint(style, self.0.to_string())
}
}

View File

@ -1,8 +1,8 @@
use ansi_term::Style;
use locale::Numeric as NumericLocale;
use crate::output::cell::TextCell;
use crate::fs::fields as f;
use crate::output::cell::TextCell;
impl f::Links {

View File

@ -1,26 +1,26 @@
use ansi_term::Style;
use crate::output::cell::TextCell;
use crate::fs::fields as f;
use crate::output::cell::TextCell;
impl f::OctalPermissions {
fn bits_to_octal(r: bool, w: bool, x: bool) -> u8 {
(r as u8) * 4 + (w as u8) * 2 + (x as u8) * 1
(r as u8) * 4 + (w as u8) * 2 + (x as u8)
}
pub fn render(&self, style: Style) -> TextCell {
let perm = &self.permissions;
let octal_sticky = Self::bits_to_octal(perm.setuid, perm.setgid, perm.sticky);
let octal_owner = Self::bits_to_octal(perm.user_read, perm.user_write, perm.user_execute);
let octal_group = Self::bits_to_octal(perm.group_read, perm.group_write, perm.group_execute);
let octal_other = Self::bits_to_octal(perm.other_read, perm.other_write, perm.other_execute);
let octal_owner = Self::bits_to_octal(perm.user_read, perm.user_write, perm.user_execute);
let octal_group = Self::bits_to_octal(perm.group_read, perm.group_write, perm.group_execute);
let octal_other = Self::bits_to_octal(perm.other_read, perm.other_write, perm.other_execute);
TextCell::paint(style, format!("{}{}{}{}", octal_sticky, octal_owner, octal_group, octal_other))
}
}
#[cfg(test)]
pub mod test {
use crate::output::cell::TextCell;

View File

@ -25,23 +25,23 @@ impl f::PermissionsPlus {
}
impl f::Permissions {
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.dash().paint("-") }
if bit { style.paint(chr) }
else { colours.dash().paint("-") }
};
vec![
bit(self.user_read, "r", colours.user_read()),
bit(self.user_write, "w", colours.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.group_read()),
bit(self.group_write, "w", colours.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.other_read()),
bit(self.other_write, "w", colours.other_write()),
bit(self.other_read, "r", colours.other_read()),
bit(self.other_write, "w", colours.other_write()),
self.other_execute_bit(colours)
]
}

View File

@ -8,38 +8,41 @@ use crate::output::table::SizeFormat;
impl f::Size {
pub fn render<C: Colours>(&self, colours: &C, size_format: SizeFormat, numerics: &NumericLocale) -> TextCell {
use number_prefix::{Prefixed, Standalone, NumberPrefix, PrefixNames};
pub fn render<C: Colours>(self, colours: &C, size_format: SizeFormat, numerics: &NumericLocale) -> TextCell {
use number_prefix::NumberPrefix;
let size = match *self {
f::Size::Some(s) => s,
f::Size::None => return TextCell::blank(colours.no_size()),
f::Size::DeviceIDs(ref ids) => return ids.render(colours),
let size = match self {
Self::Some(s) => s,
Self::None => return TextCell::blank(colours.no_size()),
Self::DeviceIDs(ref ids) => return ids.render(colours),
};
let result = match size_format {
SizeFormat::DecimalBytes => NumberPrefix::decimal(size as f64),
SizeFormat::BinaryBytes => NumberPrefix::binary(size as f64),
SizeFormat::JustBytes => {
// Use the binary prefix to select a style.
let prefix = match NumberPrefix::binary(size as f64) {
Standalone(_) => None,
Prefixed(p, _) => Some(p),
NumberPrefix::Standalone(_) => None,
NumberPrefix::Prefixed(p, _) => Some(p),
};
// But format the number directly using the locale.
let string = numerics.format_int(size);
return TextCell::paint(colours.size(prefix), string);
},
}
};
let (prefix, n) = match result {
Standalone(b) => return TextCell::paint(colours.size(None), b.to_string()),
Prefixed(p, n) => (p, n)
NumberPrefix::Standalone(b) => return TextCell::paint(colours.size(None), b.to_string()),
NumberPrefix::Prefixed(p, n) => (p, n),
};
let symbol = prefix.symbol();
let number = if n < 10f64 { numerics.format_float(n, 1) }
else { numerics.format_int(n as isize) };
let number = if n < 10_f64 { numerics.format_float(n, 1) }
else { numerics.format_int(n as isize) };
// The numbers and symbols are guaranteed to be written in ASCII, so
// we can skip the display width calculation.
@ -57,7 +60,7 @@ impl f::Size {
impl f::DeviceIDs {
fn render<C: Colours>(&self, colours: &C) -> TextCell {
fn render<C: Colours>(self, colours: &C) -> TextCell {
let major = self.major.to_string();
let minor = self.minor.to_string();
@ -72,6 +75,7 @@ impl f::DeviceIDs {
}
}
pub trait Colours {
fn size(&self, prefix: Option<Prefix>) -> Style;
fn unit(&self, prefix: Option<Prefix>) -> Style;

View File

@ -1,3 +1,5 @@
use std::time::SystemTime;
use datetime::TimeZone;
use ansi_term::Style;
@ -6,23 +8,20 @@ use crate::output::time::TimeFormat;
pub trait Render {
fn render(self, style: Style,
tz: &Option<TimeZone>,
format: &TimeFormat) -> TextCell;
fn render(self, style: Style, tz: &Option<TimeZone>, format: &TimeFormat) -> TextCell;
}
impl Render for Option<std::time::SystemTime> {
fn render(self, style: Style,
tz: &Option<TimeZone>,
format: &TimeFormat) -> TextCell {
impl Render for Option<SystemTime> {
fn render(self, style: Style, tz: &Option<TimeZone>, format: &TimeFormat) -> TextCell {
let datestamp = if let Some(time) = self {
if let Some(ref tz) = tz {
format.format_zoned(time, tz)
} else {
}
else {
format.format_local(time)
}
} else {
}
else {
String::from("-")
};

View File

@ -5,16 +5,15 @@ use crate::fs::fields as f;
use crate::output::cell::TextCell;
impl f::User {
pub fn render<C: Colours, U: Users>(&self, colours: &C, users: &U) -> 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_string_lossy().into(),
None => self.0.to_string(),
};
let style = if users.get_current_uid() == self.0 { colours.you() }
else { colours.someone_else() };
let style = if users.get_current_uid() == self.0 { colours.you() }
else { colours.someone_else() };
TextCell::paint(style, user_name)
}
}

View File

@ -1,42 +1,33 @@
use std::cmp::max;
use std::env;
use std::fmt;
use std::ops::Deref;
use std::sync::{Mutex, MutexGuard};
use datetime::TimeZone;
use zoneinfo_compiled::{CompiledData, Result as TZResult};
use log::debug;
use lazy_static::lazy_static;
use log::*;
use users::UsersCache;
use crate::style::Colours;
use crate::fs::{File, fields as f};
use crate::fs::feature::git::GitCache;
use crate::output::cell::TextCell;
use crate::output::render::TimeRender;
use crate::output::time::TimeFormat;
use crate::fs::{File, fields as f};
use crate::fs::feature::git::GitCache;
use crate::style::Colours;
/// Options for displaying a table.
#[derive(PartialEq, Debug)]
pub struct Options {
pub env: Environment,
pub size_format: SizeFormat,
pub time_format: TimeFormat,
pub 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.columns)
}
}
/// Extra columns to display in the table.
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Columns {
/// At least one of these timestamps will be shown.
@ -108,7 +99,7 @@ impl Columns {
columns.push(Column::Timestamp(TimeType::Accessed));
}
if cfg!(feature="git") && self.git && actually_enable_git {
if cfg!(feature = "git") && self.git && actually_enable_git {
columns.push(Column::GitStatus);
}
@ -118,7 +109,7 @@ impl Columns {
/// A table contains these.
#[derive(Debug)]
#[derive(Debug, Copy, Clone)]
pub enum Column {
Permissions,
FileSize,
@ -136,37 +127,38 @@ pub enum Column {
/// right-aligned, and text is left-aligned.
#[derive(Copy, Clone)]
pub enum Alignment {
Left, Right,
Left,
Right,
}
impl Column {
/// Get the alignment this column should use.
pub fn alignment(&self) -> Alignment {
match *self {
Column::FileSize
| Column::HardLinks
| Column::Inode
| Column::Blocks
| Column::GitStatus => Alignment::Right,
_ => Alignment::Left,
pub fn alignment(self) -> Alignment {
match self {
Self::FileSize |
Self::HardLinks |
Self::Inode |
Self::Blocks |
Self::GitStatus => Alignment::Right,
_ => Alignment::Left,
}
}
/// Get the text that should be printed at the top, when the user elects
/// to have a header row printed.
pub fn header(&self) -> &'static str {
match *self {
Column::Permissions => "Permissions",
Column::FileSize => "Size",
Column::Timestamp(t) => t.header(),
Column::Blocks => "Blocks",
Column::User => "User",
Column::Group => "Group",
Column::HardLinks => "Links",
Column::Inode => "inode",
Column::GitStatus => "Git",
Column::Octal => "Octal",
pub fn header(self) -> &'static str {
match self {
Self::Permissions => "Permissions",
Self::FileSize => "Size",
Self::Timestamp(t) => t.header(),
Self::Blocks => "Blocks",
Self::User => "User",
Self::Group => "Group",
Self::HardLinks => "Links",
Self::Inode => "inode",
Self::GitStatus => "Git",
Self::Octal => "Octal",
}
}
}
@ -189,8 +181,8 @@ pub enum SizeFormat {
}
impl Default for SizeFormat {
fn default() -> SizeFormat {
SizeFormat::DecimalBytes
fn default() -> Self {
Self::DecimalBytes
}
}
@ -199,6 +191,7 @@ impl Default for SizeFormat {
/// across most (all?) operating systems.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum TimeType {
/// The files modified time (`st_mtime`).
Modified,
@ -217,10 +210,10 @@ impl TimeType {
/// Returns the text to use for a columns heading in the columns output.
pub fn header(self) -> &'static str {
match self {
TimeType::Modified => "Date Modified",
TimeType::Changed => "Date Changed",
TimeType::Accessed => "Date Accessed",
TimeType::Created => "Date Created",
Self::Modified => "Date Modified",
Self::Changed => "Date Changed",
Self::Accessed => "Date Accessed",
Self::Created => "Date Created",
}
}
}
@ -229,7 +222,7 @@ impl TimeType {
/// Fields for which of a files time fields should be displayed in the
/// columns output.
///
/// There should always be at least one of these--there's no way to disable
/// There should always be at least one of these — theres no way to disable
/// the time columns entirely (yet).
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct TimeTypes {
@ -243,16 +236,19 @@ 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 { modified: true, changed: false, accessed: false, created: false }
fn default() -> Self {
Self {
modified: true,
changed: false,
accessed: false,
created: false,
}
}
}
/// The **environment** struct contains any data that could change between
/// running instances of exa, depending on the user's computer's configuration.
/// running instances of exa, depending on the users computers configuration.
///
/// Any environment field should be able to be mocked up for test runs.
pub struct Environment {
@ -260,8 +256,8 @@ pub struct Environment {
/// Localisation rules for formatting numbers.
numeric: locale::Numeric,
/// The computer's current time zone. This gets used to determine how to
/// offset files' timestamps.
/// The computers current time zone. This gets used to determine how to
/// offset files timestamps.
tz: Option<TimeZone>,
/// Mapping cache of user IDs to usernames.
@ -273,9 +269,11 @@ impl Environment {
self.users.lock().unwrap()
}
pub fn load_all() -> Self {
fn load_all() -> Self {
let tz = match determine_time_zone() {
Ok(t) => Some(t),
Ok(t) => {
Some(t)
}
Err(ref e) => {
println!("Unable to determine time zone: {}", e);
None
@ -283,22 +281,27 @@ impl Environment {
};
let numeric = locale::Numeric::load_user_locale()
.unwrap_or_else(|_| locale::Numeric::english());
.unwrap_or_else(|_| locale::Numeric::english());
let users = Mutex::new(UsersCache::new());
Environment { tz, numeric, users }
Self { tz, numeric, users }
}
}
fn determine_time_zone() -> TZResult<TimeZone> {
if let Ok(file) = env::var("TZ") {
TimeZone::from_file(format!("/usr/share/zoneinfo/{}", file))
} else {
}
else {
TimeZone::from_file("/etc/localtime")
}
}
lazy_static! {
static ref ENVIRONMENT: Environment = Environment::load_all();
}
pub struct Table<'a> {
columns: Vec<Column>,
@ -319,10 +322,14 @@ impl<'a, 'f> Table<'a> {
pub fn new(options: &'a Options, git: Option<&'a GitCache>, colours: &'a Colours) -> Table<'a> {
let columns = options.columns.collect(git.is_some());
let widths = TableWidths::zero(columns.len());
let env = &*ENVIRONMENT;
Table {
colours, widths, columns, git,
env: &options.env,
colours,
widths,
columns,
git,
env,
time_format: &options.time_format,
size_format: options.size_format,
}
@ -342,7 +349,7 @@ impl<'a, 'f> Table<'a> {
pub fn row_for_file(&self, file: &File, xattrs: bool) -> Row {
let cells = self.columns.iter()
.map(|c| self.display(file, c, xattrs))
.map(|c| self.display(file, *c, xattrs))
.collect();
Row { cells }
@ -366,29 +373,54 @@ impl<'a, 'f> Table<'a> {
}
}
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
use crate::output::table::TimeType::*;
fn display(&self, file: &File, column: Column, xattrs: bool) -> TextCell {
match column {
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::Octal => {
self.octal_permissions(file).render(self.colours.octal)
}
match *column {
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::Octal => self.octal_permissions(file).render(self.colours.octal),
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),
Column::Timestamp(TimeType::Modified) => {
file.modified_time().render(self.colours.date, &self.env.tz, self.time_format)
}
Column::Timestamp(TimeType::Changed) => {
file.changed_time().render(self.colours.date, &self.env.tz, self.time_format)
}
Column::Timestamp(TimeType::Created) => {
file.created_time().render(self.colours.date, &self.env.tz, self.time_format)
}
Column::Timestamp(TimeType::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()
@ -397,12 +429,22 @@ impl<'a, 'f> Table<'a> {
pub fn render(&self, row: Row) -> TextCell {
let mut cell = TextCell::default();
for (n, (this_cell, width)) in row.cells.into_iter().zip(self.widths.iter()).enumerate() {
let iter = row.cells.into_iter()
.zip(self.widths.iter())
.enumerate();
for (n, (this_cell, width)) in iter {
let padding = width - *this_cell.width;
match self.columns[n].alignment() {
Alignment::Left => { cell.append(this_cell); cell.add_spaces(padding); }
Alignment::Right => { cell.add_spaces(padding); cell.append(this_cell); }
Alignment::Left => {
cell.append(this_cell);
cell.add_spaces(padding);
}
Alignment::Right => {
cell.add_spaces(padding);
cell.append(this_cell);
}
}
cell.add_spaces(1);
@ -413,7 +455,6 @@ impl<'a, 'f> Table<'a> {
}
pub struct TableWidths(Vec<usize>);
impl Deref for TableWidths {
@ -425,8 +466,8 @@ impl Deref for TableWidths {
}
impl TableWidths {
pub fn zero(count: usize) -> TableWidths {
TableWidths(vec![ 0; count ])
pub fn zero(count: usize) -> Self {
Self(vec![0; count])
}
pub fn add_widths(&mut self, row: &Row) {

View File

@ -2,9 +2,11 @@
use std::time::{SystemTime, UNIX_EPOCH};
use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece};
use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece, Month};
use datetime::fmt::DateFormat;
use std::cmp;
use lazy_static::lazy_static;
use unicode_width::UnicodeWidthStr;
/// Every timestamp in exa needs to be rendered by a **time format**.
@ -23,18 +25,18 @@ use std::cmp;
///
/// 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)]
#[derive(PartialEq, Debug)]
pub enum TimeFormat {
/// The **default format** uses the users locale to print month names,
/// and specifies the timestamp down to the minute for recent times, and
/// day for older times.
DefaultFormat(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 doesnt need a locale.
ISOFormat(ISOFormat),
/// for the month so it doesnt use the locale.
ISOFormat,
/// Use the **long ISO format**, which specifies the timestamp down to the
/// minute using only numbers, without needing the locale or year.
@ -51,156 +53,65 @@ pub enum TimeFormat {
impl TimeFormat {
pub fn format_local(&self, time: SystemTime) -> String {
match *self {
TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time),
TimeFormat::ISOFormat(ref iso) => iso.format_local(time),
TimeFormat::LongISO => long_local(time),
TimeFormat::FullISO => full_local(time),
match self {
Self::DefaultFormat => default_local(time),
Self::ISOFormat => iso_local(time),
Self::LongISO => long_local(time),
Self::FullISO => full_local(time),
}
}
pub fn format_zoned(&self, time: SystemTime, zone: &TimeZone) -> String {
match *self {
TimeFormat::DefaultFormat(ref fmt) => fmt.format_zoned(time, zone),
TimeFormat::ISOFormat(ref iso) => iso.format_zoned(time, zone),
TimeFormat::LongISO => long_zoned(time, zone),
TimeFormat::FullISO => full_zoned(time, zone),
match self {
Self::DefaultFormat => default_zoned(time, zone),
Self::ISOFormat => iso_zoned(time, zone),
Self::LongISO => long_zoned(time, zone),
Self::FullISO => full_zoned(time, zone),
}
}
}
#[derive(Debug, Clone)]
pub struct DefaultFormat {
#[allow(trivial_numeric_casts)]
fn default_local(time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));
/// The year of the current time. This gets used to determine which date
/// format to use.
pub current_year: i64,
/// Localisation rules for formatting timestamps.
pub locale: locale::Time,
/// Date format for printing out timestamps that are in the current year.
pub date_and_time: DateFormat<'static>,
/// Date format for printing out timestamps that *arent*.
pub date_and_year: DateFormat<'static>,
}
impl DefaultFormat {
pub fn load() -> DefaultFormat {
use unicode_width::UnicodeWidthStr;
let locale = locale::Time::load_user_locale()
.unwrap_or_else(|_| locale::Time::english());
let current_year = LocalDateTime::now().year();
// Some locales use a three-character wide month name (Jan to Dec);
// 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(),
if date.year() == *CURRENT_YEAR {
format!("{:2} {} {:02}:{:02}",
date.day(), month_to_abbrev(date.month()),
date.hour(), date.minute())
}
else {
let date_format = match *MAXIMUM_MONTH_WIDTH {
4 => &*FOUR_WIDE_DATE_TIME,
5 => &*FIVE_WIDE_DATE_TIME,
_ => &*OTHER_WIDE_DATE_TIME,
};
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()
date_format.format(&date, &*LOCALE)
}
}
#[allow(trivial_numeric_casts)]
fn default_zoned(time: SystemTime, zone: &TimeZone) -> String {
let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
if date.year() == *CURRENT_YEAR {
format!("{:2} {} {:02}:{:02}",
date.day(), month_to_abbrev(date.month()),
date.hour(), date.minute())
}
else {
let date_format = match *MAXIMUM_MONTH_WIDTH {
4 => &*FOUR_WIDE_DATE_YEAR,
5 => &*FIVE_WIDE_DATE_YEAR,
_ => &*OTHER_WIDE_DATE_YEAR,
};
DefaultFormat { current_year, locale, date_and_time, date_and_year }
date_format.format(&date, &*LOCALE)
}
}
impl DefaultFormat {
fn is_recent(&self, date: LocalDateTime) -> bool {
date.year() == self.current_year
}
fn month_to_abbrev(month: datetime::Month) -> &'static str {
match month {
datetime::Month::January => "Jan",
datetime::Month::February => "Feb",
datetime::Month::March => "Mar",
datetime::Month::April => "Apr",
datetime::Month::May => "May",
datetime::Month::June => "Jun",
datetime::Month::July => "Jul",
datetime::Month::August => "Aug",
datetime::Month::September => "Sep",
datetime::Month::October => "Oct",
datetime::Month::November => "Nov",
datetime::Month::December => "Dec",
}
}
#[allow(trivial_numeric_casts)]
fn format_local(&self, time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));
if self.is_recent(date) {
format!("{:2} {} {:02}:{:02}",
date.day(), DefaultFormat::month_to_abbrev(date.month()),
date.hour(), date.minute())
}
else {
self.date_and_year.format(&date, &self.locale)
}
}
#[allow(trivial_numeric_casts)]
fn format_zoned(&self, time: SystemTime, zone: &TimeZone) -> String {
let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
if self.is_recent(date) {
format!("{:2} {} {:02}:{:02}",
date.day(), DefaultFormat::month_to_abbrev(date.month()),
date.hour(), date.minute())
}
else {
self.date_and_year.format(&date, &self.locale)
}
}
}
fn systemtime_epoch(time: SystemTime) -> i64 {
time
.duration_since(UNIX_EPOCH)
.map(|t| t.as_secs() as i64)
.unwrap_or_else(|e| {
let diff = e.duration();
let mut secs = diff.as_secs();
if diff.subsec_nanos() > 0 {
secs += 1;
}
-(secs as i64)
})
}
fn systemtime_nanos(time: SystemTime) -> u32 {
time
.duration_since(UNIX_EPOCH)
.map(|t| t.subsec_nanos())
.unwrap_or_else(|e| {
let nanos = e.duration().subsec_nanos();
if nanos > 0 {
1_000_000_000 - nanos
} else {
nanos
}
})
}
#[allow(trivial_numeric_casts)]
fn long_local(time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));
@ -217,7 +128,6 @@ fn long_zoned(time: SystemTime, zone: &TimeZone) -> String {
date.hour(), date.minute())
}
#[allow(trivial_numeric_casts)]
fn full_local(time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));
@ -239,55 +149,127 @@ fn full_zoned(time: SystemTime, zone: &TimeZone) -> String {
offset.hours(), offset.minutes().abs())
}
#[allow(trivial_numeric_casts)]
fn iso_local(time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));
#[derive(Debug, Clone)]
pub struct ISOFormat {
/// The year of the current time. This gets used to determine which date
/// format to use.
pub current_year: i64,
}
impl ISOFormat {
pub fn load() -> ISOFormat {
let current_year = LocalDateTime::now().year();
ISOFormat { current_year }
if is_recent(date) {
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())
}
}
impl ISOFormat {
fn is_recent(&self, date: LocalDateTime) -> bool {
date.year() == self.current_year
#[allow(trivial_numeric_casts)]
fn iso_zoned(time: SystemTime, zone: &TimeZone) -> String {
let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
if is_recent(date) {
format!("{:02}-{:02} {:02}:{:02}",
date.month() as usize, date.day(),
date.hour(), date.minute())
}
#[allow(trivial_numeric_casts)]
fn format_local(&self, time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));
if self.is_recent(date) {
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: SystemTime, zone: &TimeZone) -> String {
let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
if self.is_recent(date) {
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())
}
else {
format!("{:04}-{:02}-{:02}",
date.year(), date.month() as usize, date.day())
}
}
fn systemtime_epoch(time: SystemTime) -> i64 {
time.duration_since(UNIX_EPOCH)
.map(|t| t.as_secs() as i64)
.unwrap_or_else(|e| {
let diff = e.duration();
let mut secs = diff.as_secs();
if diff.subsec_nanos() > 0 {
secs += 1;
}
-(secs as i64)
})
}
fn systemtime_nanos(time: SystemTime) -> u32 {
time.duration_since(UNIX_EPOCH)
.map(|t| t.subsec_nanos())
.unwrap_or_else(|e| {
let nanos = e.duration().subsec_nanos();
if nanos > 0 {
1_000_000_000 - nanos
} else {
nanos
}
})
}
fn is_recent(date: LocalDateTime) -> bool {
date.year() == *CURRENT_YEAR
}
fn month_to_abbrev(month: Month) -> &'static str {
match month {
Month::January => "Jan",
Month::February => "Feb",
Month::March => "Mar",
Month::April => "Apr",
Month::May => "May",
Month::June => "Jun",
Month::July => "Jul",
Month::August => "Aug",
Month::September => "Sep",
Month::October => "Oct",
Month::November => "Nov",
Month::December => "Dec",
}
}
lazy_static! {
static ref CURRENT_YEAR: i64 = LocalDateTime::now().year();
static ref LOCALE: locale::Time = {
locale::Time::load_user_locale()
.unwrap_or_else(|_| locale::Time::english())
};
static ref MAXIMUM_MONTH_WIDTH: usize = {
// Some locales use a three-character wide month name (Jan to Dec);
// 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 = std::cmp::max(maximum_month_width, current_month_width);
}
maximum_month_width
};
static ref FOUR_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
"{2>:D} {4<:M} {2>:h}:{02>:m}"
).unwrap();
static ref FIVE_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
"{2>:D} {5<:M} {2>:h}:{02>:m}"
).unwrap();
static ref OTHER_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
"{2>:D} {:M} {2>:h}:{02>:m}"
).unwrap();
static ref FOUR_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse(
"{2>:D} {4<:M} {5>:Y}"
).unwrap();
static ref FIVE_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse(
"{2>:D} {5<:M} {5>:Y}"
).unwrap();
static ref OTHER_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse(
"{2>:D} {:M} {5>:Y}"
).unwrap();
}

View File

@ -39,7 +39,7 @@
//! each directory)
#[derive(PartialEq, Debug, Clone)]
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum TreePart {
/// Rightmost column, *not* the last in the directory.
@ -59,12 +59,12 @@ impl TreePart {
/// Turn this tree part into ASCII-licious box drawing characters!
/// (Warning: not actually ASCII)
pub fn ascii_art(&self) -> &'static str {
match *self {
TreePart::Edge => "├──",
TreePart::Line => "",
TreePart::Corner => "└──",
TreePart::Blank => " ",
pub fn ascii_art(self) -> &'static str {
match self {
Self::Edge => "├──",
Self::Line => "",
Self::Corner => "└──",
Self::Blank => " ",
}
}
}
@ -111,18 +111,21 @@ impl TreeTrunk {
// If this isnt our first iteration, then update the tree parts thus
// far to account for there being another row after it.
if let Some(last) = self.last_params {
self.stack[last.depth.0] = if last.last { TreePart::Blank } else { TreePart::Line };
self.stack[last.depth.0] = if last.last { TreePart::Blank }
else { TreePart::Line };
}
// Make sure the stack has enough space, then add or modify another
// part into it.
self.stack.resize(params.depth.0 + 1, TreePart::Edge);
self.stack[params.depth.0] = if params.last { TreePart::Corner } else { TreePart::Edge };
self.stack[params.depth.0] = if params.last { TreePart::Corner }
else { TreePart::Edge };
self.last_params = Some(params);
// Return the tree parts as a slice of the stack.
//
// Ignore the first element here to prevent a 'zeroth level' from
// Ignore the first element here to prevent a zeroth level from
// appearing before the very first directory. This level would
// join unrelated directories without connecting to anything:
//
@ -138,8 +141,8 @@ impl TreeTrunk {
}
impl TreeParams {
pub fn new(depth: TreeDepth, last: bool) -> TreeParams {
TreeParams { depth, last }
pub fn new(depth: TreeDepth, last: bool) -> Self {
Self { depth, last }
}
pub fn is_at_root(&self) -> bool {
@ -148,18 +151,19 @@ impl TreeParams {
}
impl TreeDepth {
pub fn root() -> TreeDepth {
TreeDepth(0)
pub fn root() -> Self {
Self(0)
}
pub fn deeper(self) -> TreeDepth {
TreeDepth(self.0 + 1)
pub fn deeper(self) -> Self {
Self(self.0 + 1)
}
/// Creates an iterator that, as well as yielding each value, yields a
/// `TreeParams` with the current depth and last flag filled in.
pub fn iterate_over<I, T>(self, inner: I) -> Iter<I>
where I: ExactSizeIterator+Iterator<Item=T> {
where I: ExactSizeIterator + Iterator<Item = T>
{
Iter { current_depth: self, inner }
}
}
@ -171,14 +175,16 @@ pub struct Iter<I> {
}
impl<I, T> Iterator for Iter<I>
where I: ExactSizeIterator+Iterator<Item=T> {
where I: ExactSizeIterator + Iterator<Item = T>
{
type Item = (TreeParams, T);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|t| {
// use exact_size_is_empty API soon
(TreeParams::new(self.current_depth, self.inner.len() == 0), t)
})
let t = self.inner.next()?;
// TODO: use exact_size_is_empty API soon
let params = TreeParams::new(self.current_depth, self.inner.len() == 0);
Some((params, t))
}
}
@ -194,20 +200,20 @@ mod trunk_test {
#[test]
fn empty_at_first() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, true)), &[]);
assert_eq!(tt.new_row(params(0, true)), &[ ]);
}
#[test]
fn one_child() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, true)), &[]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(params(0, true)), &[ ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
#[test]
fn two_children() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, true)), &[]);
assert_eq!(tt.new_row(params(0, true)), &[ ]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
@ -215,11 +221,11 @@ mod trunk_test {
#[test]
fn two_times_two_children() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, false)), &[]);
assert_eq!(tt.new_row(params(0, false)), &[ ]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(params(0, true)), &[]);
assert_eq!(tt.new_row(params(0, true)), &[ ]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
@ -227,7 +233,7 @@ mod trunk_test {
#[test]
fn two_times_two_nested_children() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, true)), &[]);
assert_eq!(tt.new_row(params(0, true)), &[ ]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(2, false)), &[ TreePart::Line, TreePart::Edge ]);
@ -240,7 +246,6 @@ mod trunk_test {
}
#[cfg(test)]
mod iter_test {
use super::*;

View File

@ -1,9 +1,8 @@
use ansi_term::Style;
use ansi_term::Colour::{Red, Green, Yellow, Blue, Cyan, Purple, Fixed};
use ansi_term::Style;
use crate::output::render;
use crate::output::file_name::Colours as FileNameColours;
use crate::output::render;
use crate::style::lsc::Pair;
@ -109,12 +108,12 @@ pub struct Git {
}
impl Colours {
pub fn plain() -> Colours {
Colours::default()
pub fn plain() -> Self {
Self::default()
}
pub fn colourful(scale: bool) -> Colours {
Colours {
pub fn colourful(scale: bool) -> Self {
Self {
colourful: true,
filekinds: FileKinds {
@ -190,11 +189,8 @@ impl Colours {
impl Size {
pub fn colourful(scale: bool) -> Self {
if scale {
Self::colourful_scale()
} else {
Self::colourful_plain()
}
if scale { Self::colourful_scale() }
else { Self::colourful_plain() }
}
fn colourful_plain() -> Self {
@ -233,7 +229,6 @@ impl Size {
unit_giga: Green.normal(),
unit_huge: Green.normal(),
}
}
}
@ -352,6 +347,7 @@ impl Colours {
_ => return false,
}
true
}
@ -431,10 +427,11 @@ impl render::PermissionsColours for Colours {
impl render::SizeColours for Colours {
fn size(&self, prefix: Option<number_prefix::Prefix>) -> Style {
use number_prefix::Prefix::*;
match prefix {
None => self.size.number_byte,
Some(Kilo) | Some(Kibi) => self.size.number_kilo,
Some(Mega) | Some(Mibi) => self.size.number_mega,
Some(Mega) | Some(Mebi) => self.size.number_mega,
Some(Giga) | Some(Gibi) => self.size.number_giga,
Some(_) => self.size.number_huge,
}
@ -442,10 +439,11 @@ impl render::SizeColours for Colours {
fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
use number_prefix::Prefix::*;
match prefix {
None => self.size.unit_byte,
Some(Kilo) | Some(Kibi) => self.size.unit_kilo,
Some(Mega) | Some(Mibi) => self.size.unit_mega,
Some(Mega) | Some(Mebi) => self.size.unit_mega,
Some(Giga) | Some(Gibi) => self.size.unit_giga,
Some(_) => self.size.unit_huge,
}

View File

@ -1,3 +1,4 @@
use std::iter::Peekable;
use std::ops::FnMut;
use ansi_term::{Colour, Style};
@ -21,31 +22,28 @@ use ansi_term::Colour::*;
// 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>) -> () {
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() {
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> {
where I: Iterator<Item = &'a str>
{
match iter.peek() {
Some(&"5") => {
let _ = iter.next();
@ -55,11 +53,12 @@ where I: Iterator<Item=&'a str> {
}
}
}
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.
// but this clashes with splitting on : in each_pair above.
/*if hexes.contains(':') {
let rgb = hexes.splitn(3, ':').collect::<Vec<_>>();
if rgb.len() != 3 {
@ -77,11 +76,19 @@ where I: Iterator<Item=&'a str> {
}
}
}
_ => {},
}
None
}
pub struct Pair<'var> {
pub key: &'var str,
pub value: &'var str,
}
impl<'var> Pair<'var> {
pub fn to_style(&self) -> Style {
let mut style = Style::default();
@ -123,7 +130,7 @@ impl<'var> Pair<'var> {
"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 */},
_ => {/* ignore the error and do nothing */},
}
}
@ -183,7 +190,6 @@ mod ansi_test {
}
#[cfg(test)]
mod test {
use super::*;