mirror of
https://github.com/Llewellynvdm/exa.git
synced 2024-06-01 07:00:46 +00:00
Merge branch 'modernise'
This commit is contained in:
commit
f42957fab8
1
.rustfmt.toml
Normal file
1
.rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
disable_all_formatting = true
|
450
Cargo.lock
generated
450
Cargo.lock
generated
|
@ -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"
|
||||
|
|
39
Cargo.toml
39
Cargo.toml
|
@ -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
44
Justfile
Normal 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
|
86
Makefile
86
Makefile
|
@ -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
67
Vagrantfile
vendored
|
@ -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 wasn’t actually
|
||||
# self-contained? Or the problem where the Linux binary was actually the
|
||||
# macOS binary in disguise?
|
||||
#
|
||||
# This is a “fresh” VM that intentionally downloads no dependencies and
|
||||
# installs nothing so that we can check that exa still runs!
|
||||
config.vm.define(:fresh) do |config|
|
||||
config.vm.box = UBUNTU
|
||||
config.vm.hostname = 'fresh'
|
||||
|
||||
config.vm.provider :virtualbox do |v|
|
||||
v.name = 'exa-fresh'
|
||||
v.memory = 384
|
||||
v.cpus = 1
|
||||
end
|
||||
|
||||
# Well, we do need *one* dependency...
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
apt-get install -qq -o=Dpkg::Use-Pty=0 -y unzip
|
||||
EOF
|
||||
|
||||
# This thing also has its own welcoming text.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
rm -f /etc/update-motd.d/*
|
||||
|
||||
# Capture the help text so it gets displayed first
|
||||
bash /vagrant/devtools/dev-help-testvm.sh > /etc/motd
|
||||
|
||||
# Disable last login date in sshd
|
||||
sed -i '/PrintLastLog yes/c\PrintLastLog no' /etc/ssh/sshd_config
|
||||
systemctl restart sshd
|
||||
EOF
|
||||
|
||||
# Make the checker script a command.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
echo -e "#!/bin/sh\nbash /vagrant/devtools/dev-download-and-check-release.sh \"\\$*\"" > /usr/bin/check-release
|
||||
chmod +x /usr/bin/check-release
|
||||
EOF
|
||||
end
|
||||
end
|
||||
|
|
8
build.rs
8
build.rs
|
@ -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())
|
||||
};
|
||||
|
||||
|
|
|
@ -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 you’re able to run from your main computer! They’re intended to be run from the Vagrant machines — they have commands such as ‘package-exa’ or ‘check-release’ that execute them instead.
|
||||
They are **not general-purpose scripts** that you’re able to run from your main computer! They’re intended to be run from the Vagrant machine.
|
||||
|
|
|
@ -12,7 +12,7 @@ bash /vagrant/devtools/dev-versions.sh
|
|||
# The Cool Prompt tells you whether you’re in debug or strict mode, whether
|
||||
# you have colours configured, and whether your last command failed.
|
||||
function nonzero_return() { RETVAL=$?; [ $RETVAL -ne 0 ] && echo "$RETVAL "; }
|
||||
function debug_mode() { [ -n "$EXA_DEBUG" ] && echo "debug "; }
|
||||
function 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 exa’s debugging logs.
|
||||
function debug () {
|
||||
case "$1" in "on") export EXA_DEBUG=1 ;;
|
||||
"off") export EXA_DEBUG= ;;
|
||||
"") [ -n "$EXA_DEBUG" ] && echo "debug on" || echo "debug off" ;;
|
||||
*) echo "Usage: debug on|off"; return 1 ;; esac; }
|
||||
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 you’d like exa’s 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. There’s 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;
|
||||
}
|
||||
|
|
|
@ -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 it’s the minimum version that can be tested.
|
||||
|
||||
set +x
|
||||
trap 'exit' ERR
|
||||
|
||||
exa_version=$1
|
||||
if [[ -z "$exa_version" ]]; then
|
||||
echo "Please specify a version, such as '$0 0.8.0'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Delete anything that already exists
|
||||
rm -rfv "/tmp/${exa_version}-downloads"
|
||||
|
||||
|
||||
# Create a temporary directory and download exa into it
|
||||
mkdir "/tmp/${exa_version}-downloads"
|
||||
cd "/tmp/${exa_version}-downloads"
|
||||
|
||||
echo -e "\n\033[4mDownloading stuff...\033[0m"
|
||||
wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/exa-macos-x86_64-${exa_version}.zip"
|
||||
wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/exa-linux-x86_64-${exa_version}.zip"
|
||||
|
||||
wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/MD5SUMS"
|
||||
wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/SHA1SUMS"
|
||||
|
||||
|
||||
# Unzip the zips and check the sums
|
||||
echo -e "\n\033[4mExtracting that stuff...\033[0m"
|
||||
unzip "exa-macos-x86_64-${exa_version}.zip"
|
||||
unzip "exa-linux-x86_64-${exa_version}.zip"
|
||||
|
||||
echo -e "\n\033[4mValidating MD5 checksums...\033[0m"
|
||||
md5sum -c MD5SUMS
|
||||
|
||||
echo -e "\n\033[4mValidating SHA1 checksums...\033[0m"
|
||||
sha1sum -c SHA1SUMS
|
||||
|
||||
|
||||
# Finally, give the Linux version a go
|
||||
echo -e "\n\033[4mChecking it actually runs...\033[0m"
|
||||
./"exa-linux-x86_64" --version
|
||||
./"exa-linux-x86_64" --long
|
||||
|
||||
echo -e "\n\033[1;32mAll's lookin' good!\033[0m"
|
|
@ -1,15 +0,0 @@
|
|||
# This script generates the MD5SUMS and SHA1SUMS files.
|
||||
# You’ll need to have run ‘dev-download-and-check-release.sh’ and
|
||||
# ‘local-package-for-macos.sh’ scripts to generate the binaries first.
|
||||
|
||||
set +x
|
||||
trap 'exit' ERR
|
||||
|
||||
cd /vagrant
|
||||
rm -f MD5SUMS SHA1SUMS
|
||||
|
||||
echo -e "\n\033[4mValidating MD5 checksums...\033[0m"
|
||||
md5sum exa-linux-x86_64 exa-macos-x86_64 | tee MD5SUMS
|
||||
|
||||
echo -e "\n\033[4mValidating SHA1 checksums...\033[0m"
|
||||
sha1sum exa-linux-x86_64 exa-macos-x86_64 | tee SHA1SUMS
|
|
@ -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
|
||||
"
|
|
@ -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 can’t be done in exa’s own option parsing because that part of it
|
||||
/// logs as well, so by the time execution gets there, the logger needs to
|
||||
/// have already been set up.
|
||||
pub fn configure_logger() {
|
||||
extern crate env_logger;
|
||||
extern crate log;
|
||||
|
||||
let present = match var_os(exa::vars::EXA_DEBUG) {
|
||||
Some(debug) => debug.len() > 0,
|
||||
None => false,
|
||||
};
|
||||
|
||||
let mut logs = env_logger::Builder::new();
|
||||
if present {
|
||||
logs.filter(None, log::LevelFilter::Debug);
|
||||
}
|
||||
else {
|
||||
logs.filter(None, log::LevelFilter::Off);
|
||||
}
|
||||
|
||||
logs.init()
|
||||
}
|
||||
|
||||
|
||||
extern crate libc;
|
||||
#[allow(trivial_numeric_casts)]
|
||||
mod exits {
|
||||
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;
|
||||
}
|
227
src/exa.rs
227
src/exa.rs
|
@ -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 that’s
|
||||
/// `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 isn’t 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 they’re actually listed, if the options demand it.
|
||||
fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
|
||||
if options.should_scan_for_git() {
|
||||
Some(args.iter().map(PathBuf::from).collect())
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
|
||||
pub fn from_args<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire>
|
||||
where I: Iterator<Item=&'args OsString> {
|
||||
Options::parse(args, &LiveVars).map(move |(options, mut args)| {
|
||||
debug!("Dir action from arguments: {:#?}", options.dir_action);
|
||||
debug!("Filter from arguments: {:#?}", options.filter);
|
||||
debug!("View from arguments: {:#?}", options.view.mode);
|
||||
|
||||
// List the current directory by default, like ls.
|
||||
// This has to be done here, otherwise git_options won’t see it.
|
||||
if args.is_empty() {
|
||||
args = vec![ OsStr::new(".") ];
|
||||
}
|
||||
|
||||
let git = git_options(&options, &args);
|
||||
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 directory’s name before we list it, *except* in
|
||||
// the case where it’s the only directory, *except* if there are any
|
||||
// files to print as well. (It’s 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(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 that’s
|
||||
/// 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 doesn’t actually yield the `.` and `..`
|
||||
/// entries, so if the user wants to see them, we’ll have to add them
|
||||
/// ourselves after the files have been read.
|
||||
pub fn read_dir(path: PathBuf) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 we’ve 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 we’ve extracted from the repository, but only after we’ve
|
||||
/// 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 don’t need to
|
||||
/// re-query the entire repository the times after that.
|
||||
///
|
||||
/// The temporary `Processing` enum variant is used after the `git2`
|
||||
/// repository is moved out, but before the results have been moved in!
|
||||
/// See https://stackoverflow.com/q/45985827/3484614
|
||||
/// 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 doesn’t 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 don’t really have an ‘official’ status.
|
||||
fn dir_status(&self, dir: &Path) -> f::Git {
|
||||
let path = reorient(dir);
|
||||
let s = self.statuses.iter()
|
||||
.filter(|p| p.0.starts_with(&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
|
||||
/// you’d 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;
|
||||
// I’m not 100% on this func tbh
|
||||
|
||||
// TODO: I’m 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 file’s 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, it’s 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 file’s 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 file’s 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 can’t think of a time when I’ve seen it and
|
||||
/// data is rarely useful — I can’t think of a time when I’ve 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 file’s status in a Git repository. Whether a file is in a repository or
|
||||
/// not is handled by the Git module, rather than having a “null” variant in
|
||||
/// this enum.
|
||||
#[derive(PartialEq)]
|
||||
#[derive(PartialEq, Copy, Clone)]
|
||||
pub enum GitStatus {
|
||||
|
||||
/// This file hasn’t changed since the last commit.
|
||||
|
@ -207,23 +220,27 @@ pub enum GitStatus {
|
|||
/// A file that’s ignored (that matches a line in .gitignore)
|
||||
Ignored,
|
||||
|
||||
/// A file that's updated but unmerged.
|
||||
/// A file that’s updated but unmerged.
|
||||
Conflicted,
|
||||
}
|
||||
|
||||
|
||||
/// A file’s complete Git status. It’s possible to make changes to a file, add
|
||||
/// it to the staging area, then make *more* changes, so we need to list each
|
||||
/// file’s 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 Rust’s `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.
|
||||
/// it’s 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
|
||||
/// directory’s children, and are in fact added specifically by exa; this
|
||||
/// means that they should be skipped when recursing.
|
||||
pub is_all_all: bool,
|
||||
}
|
||||
|
||||
impl<'dir> File<'dir> {
|
||||
pub fn 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 file’s 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 shouldn’t be used to check
|
||||
/// if a `File` is a directory or not! For that, just use `is_directory()`.
|
||||
pub fn to_dir(&self) -> IOResult<Dir> {
|
||||
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 file’s inode.
|
||||
pub fn inode(&self) -> f::Inode {
|
||||
f::Inode(self.metadata.ino())
|
||||
}
|
||||
|
||||
/// This file's number of filesystem blocks.
|
||||
/// This file’s 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 don’t 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 file’s last accessed timestamp, if available on this platform.
|
||||
|
@ -392,7 +399,7 @@ impl<'dir> File<'dir> {
|
|||
/// This file’s 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 file’s 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 isn’t a link to begin with, but also if, say, we don’t 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 doesn’t 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` type’s 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;
|
||||
|
|
|
@ -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 file’s 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 don’t
|
||||
/// parse correctly are returned separately.
|
||||
/// are valid glob patterns into an `IgnorePatterns`. The inputs that
|
||||
/// don’t parse correctly are returned separately.
|
||||
pub fn parse_from_iter<'a, I: IntoIterator<Item = &'a str>>(iter: I) -> (Self, Vec<glob::PatternError>) {
|
||||
let iter = iter.into_iter();
|
||||
|
||||
|
@ -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::*;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
/// don’t 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 there’s no extension, either!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
66
src/logger.rs
Normal file
66
src/logger.rs
Normal 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
280
src/main.rs
Normal 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 won’t 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 isn’t 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 they’re actually listed, if the options demand it.
|
||||
fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
|
||||
if options.should_scan_for_git() {
|
||||
Some(args.iter().map(PathBuf::from).collect())
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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 directory’s name before we list it, *except* in
|
||||
// the case where it’s the only directory, *except* if there are any
|
||||
// files to print as well. (It’s 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;
|
||||
}
|
|
@ -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 wouldn’t 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 {
|
|||
/// flag’s value, and whether the `--tree` flag was passed, which was
|
||||
/// determined earlier. The maximum level should be a number, and this
|
||||
/// will fail with an `Err` if it isn’t.
|
||||
pub fn deduce(matches: &MatchedFlags, tree: bool) -> Result<RecurseOptions, Misfire> {
|
||||
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
109
src/options/error.rs
Normal 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
|
||||
/// 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 can’t 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(", "))
|
||||
}
|
||||
}
|
|
@ -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 argument’s value can be one of several flags, listed above.
|
||||
/// Returns the default sort field if none is given, or `Err` if the
|
||||
/// value doesn’t correspond to a sort field we know about.
|
||||
fn deduce(matches: &MatchedFlags) -> Result<SortField, Misfire> {
|
||||
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 can’t 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, doesn’t 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` won’t 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` argument’s value. This is a list of strings
|
||||
/// separated by pipe (`|`) characters, given in any order.
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<IgnorePatterns, Misfire> {
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<Self, OptionsError> {
|
||||
|
||||
// If there are no inputs, we return a set of patterns that doesn’t
|
||||
// match anything, rather than, say, `None`.
|
||||
let inputs = match matches.get(&flags::IGNORE_GLOB)? {
|
||||
None => return Ok(IgnorePatterns::empty()),
|
||||
Some(is) => is,
|
||||
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'))));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 section’s 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 don’t do any strict-mode error checking here: it’s OK to give
|
||||
/// the --help or --long flags more than once. Actually checking for
|
||||
/// errors when the user wants help is kind of petty!
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<(), HelpString> {
|
||||
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 isn’t passed
|
||||
let args = vec![];
|
||||
let opts = Options::parse(args, &None);
|
||||
assert!(! matches!(opts, OptionsResult::Help(_))) // no help when --help isn’t passed
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 program’s normal execution.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum Misfire {
|
||||
|
||||
/// The getopts crate didn’t 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 isn’t strictly an error, which is why
|
||||
/// this enum isn’t 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 can’t 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(", "))
|
||||
}
|
||||
}
|
|
@ -60,7 +60,7 @@
|
|||
//!
|
||||
//! `--sort=size` should override `--sort=Name` because it’s closer to the end
|
||||
//! of the arguments array. In fact, because there’s no way to tell where the
|
||||
//! arguments came from -- it’s just a heuristic -- this will still work even
|
||||
//! arguments came from — it’s just a heuristic — this will still work even
|
||||
//! if no aliases are being used!
|
||||
//!
|
||||
//! Finally, this isn’t just useful when options could override each other.
|
||||
|
@ -69,32 +69,32 @@
|
|||
//! it’s 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 they’ve 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(¬hing, &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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 don’t actually
|
||||
/// store the user’s input after it’s been matched to a flag, we just store
|
||||
/// which flag it was.
|
||||
/// the arguments will all be unchecked `OsString` values, because we don’t
|
||||
/// actually store the user’s input after it’s been matched to a flag, we just
|
||||
/// store which flag it was.
|
||||
pub type LongArg = &'static str;
|
||||
|
||||
/// A **list of values** that an option can have, to be displayed when the
|
||||
|
@ -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 user’s 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
|
||||
// doesn’t 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 weren’t 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 wasn’t, and an error in
|
||||
/// strict mode if it was specified more than once.
|
||||
pub fn has(&self, arg: &'static Arg) -> Result<bool, Misfire> {
|
||||
self.has_where(|flag| flag.matches(arg)).map(|flag| flag.is_some())
|
||||
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.
|
||||
///
|
||||
/// You’ll have to test the resulting flag to see which argument it was.
|
||||
pub fn has_where<P>(&self, predicate: P) -> Result<Option<&Flag>, Misfire>
|
||||
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 wasn’t, and an error in strict mode if it was specified more
|
||||
/// than once.
|
||||
pub fn get(&self, arg: &'static Arg) -> Result<Option<&OsStr>, Misfire> {
|
||||
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.
|
||||
///
|
||||
/// It’s not possible to tell which flag the value belonged to from this.
|
||||
pub fn get_where<P>(&self, predicate: P) -> Result<Option<&OsStr>, Misfire>
|
||||
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 },
|
||||
}
|
||||
|
||||
// It’s technically possible for ParseError::UnknownArgument to borrow its
|
||||
// OsStr rather than owning it, but that would give ParseError a lifetime,
|
||||
// which would give Misfire a lifetime, which gets used everywhere. And this
|
||||
// only happens when an error occurs, so it’s not really worth it.
|
||||
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 isn’t 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 isn’t even used
|
||||
let bits = $inputs.as_ref().into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
|
||||
let got = Args(TEST_ARGS).parse(bits.iter(), strictness);
|
||||
let inputs = $inputs.iter().map(OsStr::new);
|
||||
|
||||
let strictness = Strictness::UseLastArguments; // this isn’t 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 exa’s 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>;
|
||||
|
|
|
@ -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 aren’t 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 doesn’t bother checking for errors.
|
||||
pub fn deduce(matches: &MatchedFlags) -> Result<(), VersionString> {
|
||||
/// Like --help, this doesn’t 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(_)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 view’s 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: I’m not sure if the code even gets this far.
|
||||
// There is an identical check in dir_action
|
||||
return Err(Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
|
||||
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 doesn’t 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 {
|
|||
/// It’s valid to show more than one column by passing in more than one
|
||||
/// option, but passing *no* options means that the user just wants to
|
||||
/// see the default set.
|
||||
fn deduce(matches: &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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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` — it’s 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! The **Details** output view displays each file as a row in a table.
|
||||
//!
|
||||
//! It's used in the following situations:
|
||||
//! It’s 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 file’s 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 can’t find a way around it:
|
||||
// https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6
|
||||
let mut table = Some(table);
|
||||
self.add_files_to_table(&mut 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 file’s 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 it’s not! In a grid view, where there's no space to display
|
||||
// but it’s not! In a grid view, where there’s no space to display
|
||||
// link targets, the filename has to have a different style to
|
||||
// indicate this fact. But when showing targets, we can just
|
||||
// colour the path instead (see below), and leave the broken
|
||||
|
@ -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 there’s 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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 it’s harder to see what’s 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 that’s being generated.
|
||||
///
|
||||
/// This includes an empty files vector because the files get added to
|
||||
/// the table in *this* file, not in details: we only want to insert every
|
||||
/// *n* files into each column’s table, not all of them.
|
||||
pub fn details(&self) -> DetailsRender<'a> {
|
||||
DetailsRender {
|
||||
dir: self.dir,
|
||||
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 doesn’t take an IgnoreCache even though the details one does
|
||||
// because grid-details has no tree view.
|
||||
|
||||
pub fn render<W: Write>(self, git: Option<&GitCache>, w: &mut W) -> IOResult<()> {
|
||||
if let Some((grid, width)) = self.find_fitting_grid(git) {
|
||||
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 can’t fit everything in a grid 100 columns wide, then
|
||||
// something has gone seriously awry
|
||||
for column_count in 2..100 {
|
||||
let grid = self.make_grid(column_count, options, git, &file_names, rows.clone(), &drender);
|
||||
let 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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("?"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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("-")
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 file’s modified time (`st_mtime`).
|
||||
Modified,
|
||||
|
||||
|
@ -217,10 +210,10 @@ impl TimeType {
|
|||
/// Returns the text to use for a column’s heading in the columns output.
|
||||
pub fn header(self) -> &'static str {
|
||||
match self {
|
||||
TimeType::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 file’s 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 — there’s 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 user’s computer’s 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 computer’s 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) {
|
||||
|
|
|
@ -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 user’s locale to print month names,
|
||||
/// and specifies the timestamp down to the minute for recent times, and
|
||||
/// day for older times.
|
||||
DefaultFormat(DefaultFormat),
|
||||
DefaultFormat,
|
||||
|
||||
/// Use the **ISO format**, which specifies the timestamp down to the
|
||||
/// minute for recent times, and day for older times. It uses a number
|
||||
/// for the month so it doesn’t need a locale.
|
||||
ISOFormat(ISOFormat),
|
||||
/// for the month so it doesn’t 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 *aren’t*.
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -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 isn’t 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::*;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
Loading…
Reference in New Issue
Block a user