mirror of https://github.com/Llewellynvdm/exa.git
Compare commits
433 Commits
Author | SHA1 | Date |
---|---|---|
Aria | 753150d374 | |
capak07 | d7c9f4e65f | |
Mélanie Chauvel | fb05c421ae | |
Mélanie Chauvel | f039202b4f | |
Mélanie Chauvel | 17893b5f57 | |
Mélanie Chauvel | 08cc56d7ac | |
Mélanie Chauvel | 352d32b60c | |
Mélanie Chauvel | e79f7361a7 | |
dawg | a4cee84b4b | |
1stDimension | bf41cdefa6 | |
Matthew Toohey | 94043e1fa8 | |
Daniel Cervenkov | b8cee60acf | |
Mélanie Chauvel | c697d06670 | |
Victor Song | ee67110333 | |
Victor Song | 39d15a317d | |
Victor Song | 8d03922e3b | |
Victor Song | eba3646b83 | |
Victor Song | 72b2119a34 | |
Victor Song | bbea87db91 | |
Victor Song | 1dc14eaff1 | |
Victor Song | d24ca084a3 | |
Victor Song | 433a9a52d3 | |
Victor Song | 7c1878f0e4 | |
Victor Song | 1f409793ae | |
Victor Song | 19601267cf | |
Victor Song | 7595289698 | |
Victor Song | 2917062466 | |
Victor Song | 1b844a8dfa | |
Victor Song | af267ba638 | |
Victor Song | cd715a6e00 | |
Victor Song | 89bcc00e32 | |
Mélanie Chauvel | f3ca1fe6f7 | |
Mélanie Chauvel | e385cd58da | |
Mélanie Chauvel | 3ca40915ae | |
cab-1729 | 45b6413fd0 | |
TygrisIQ | 577ac91513 | |
TygrisIQ | d40b7b1ff4 | |
TygrisIQ | 21758c81ea | |
Mélanie Chauvel | 7e4944c188 | |
TygrisIQ | 584b53bb17 | |
TygrisIQ | a4b23055a8 | |
TygrisIQ | 863d96150d | |
cab-1729 | a65c52d821 | |
cab-1729 | 954462634a | |
Mélanie Chauvel | 8ad8b33573 | |
Mélanie Chauvel | 8c2956a8fd | |
Mélanie Chauvel | 29422d8c93 | |
Chester Liu | 6fb3740f24 | |
Chester Liu | 53cb75cf2b | |
Chester Liu | d6732aea10 | |
Mélanie Chauvel | dceca33779 | |
Ryooooooga | 6197006d5f | |
Mélanie Chauvel | 02f44c68d8 | |
ewreurei | 98a4431d6a | |
Mélanie Chauvel | fc6a6d0b38 | |
Mélanie Chauvel | ccc1f9999a | |
Shun Sakai | bced9841f4 | |
Shun Sakai | f5bbfa7871 | |
Mélanie Chauvel | b869bd06dc | |
Mélanie Chauvel | f6db28e25a | |
Mélanie Chauvel | 0659c36897 | |
Mélanie Chauvel | a58a3313ea | |
Philippe Eberli | fe64690063 | |
Abhilash Balaji | c968c388d4 | |
Ricardo Pérez | 400bb0a140 | |
Wesley | f0b9cceb73 | |
Jacob Vaverka | e433b3fed0 | |
Christian Höltje | 42659f9345 | |
Mélanie Chauvel | a2f3ff98c2 | |
Mélanie Chauvel | 4f919a6bc5 | |
Mélanie Chauvel | 446903ac00 | |
Mélanie Chauvel | be6feb82d8 | |
KOSHIKAWA Kenichi | 352a1e8f19 | |
Sam James | 89d537adb4 | |
Mélanie Chauvel | 4f0275395b | |
Loïc | c3eb8321ff | |
cab-1729 | 091ab51b98 | |
Ashin Antony | 859666d287 | |
Scott B | c4b8e7af1a | |
Scott B | 69ae5db3b6 | |
Mélanie Chauvel | 3e9de0e7e1 | |
Bastien Orivel | af208285e8 | |
Mélanie Chauvel | ef8fd32dc6 | |
Mélanie Chauvel | 2ac7024197 | |
Mélanie Chauvel | 7c957f95b3 | |
Max Zhuravsky | c6874f0b32 | |
Mélanie Chauvel | df4fb84ae1 | |
Max Zhuravsky | a371c41711 | |
Max Zhuravsky | aab1d3db59 | |
Chester Liu | 99d653b7fa | |
Mélanie Chauvel | b32f441851 | |
Mélanie Chauvel | 0332e0c7f7 | |
Mélanie Chauvel | 0d645735d7 | |
Mélanie Chauvel | 6af9e221a4 | |
Mélanie Chauvel | 0cebf3ad1c | |
jim4067 | 4220b6f41e | |
Mélanie Chauvel | a7aca35d97 | |
Mélanie Chauvel | 35aeac759e | |
jim4067 | 247d1345e7 | |
hellosway | 1c36b71779 | |
hellosway | 659def7138 | |
Mélanie Chauvel | ec786201c8 | |
ariasuni | d2b6cc9185 | |
ariasuni | 75f14d23a3 | |
Joshua Gawley | 11793973fe | |
Mélanie Chauvel | 257786749f | |
Mélanie Chauvel | fe11b9d319 | |
Mélanie Chauvel | aff35a1643 | |
Izhak Jakov | 5f49a2e840 | |
Mélanie Chauvel | 439b629d90 | |
Mélanie Chauvel | 26b40bf773 | |
Bill Risher | 79cd5d448a | |
a1346054 | 2bef43fb1b | |
a1346054 | 91dcf52972 | |
Mélanie Chauvel | 3f24f7cbcf | |
Mélanie Chauvel | 4c8658ab90 | |
Mélanie Chauvel | e7a477eb15 | |
Mélanie Chauvel | 69d5e1fc11 | |
Mélanie Chauvel | c24afe3a08 | |
Mélanie Chauvel | 049f766d1d | |
Freed-Wu | 4b6cf1b5a4 | |
James Tai | c46329efb2 | |
xxkfqz | 8de5b97804 | |
Mélanie Chauvel | dc5c42a0f2 | |
Mélanie Chauvel | 78b46e219e | |
Kid | f6b3975562 | |
Matthew Gleich | 6a07b59a80 | |
Chester Liu | 9881d00d00 | |
Mélanie Chauvel | a6754f3cc3 | |
Matthew Gleich | 56c78400b8 | |
Mélanie Chauvel | 42b546606e | |
Chester Liu | e3204a574e | |
Chester Liu | 23a1c8a41f | |
ariasuni | 785d6ed991 | |
Mélanie Chauvel | b18e93d283 | |
ariasuni | 045172bd9e | |
Mélanie Chauvel | f8610d05ae | |
ariasuni | 86d5939abe | |
ariasuni | 90416ed3ce | |
ariasuni | 7c80070120 | |
Mélanie Chauvel | a58ad6487f | |
Christian Göttsche | ae62f5d18e | |
Christian Göttsche | d253893614 | |
Christian Göttsche | 61ec153bcd | |
ariasuni | 4a81d2df91 | |
Mélanie Chauvel | 95682f5674 | |
Mélanie Chauvel | 6b8d7fcd70 | |
skyline75489 | 76e336c757 | |
ariasuni | a85c72e2a0 | |
Haren S | 90b97753ad | |
Prunkles | 7a26b4e0f7 | |
Chester Liu | d6d35bf47e | |
Chester Liu | 777cd7e815 | |
Chester Liu | 8f0e4ccfdd | |
Benjamin Sago | 293372a613 | |
Benjamin Sago | c0df1fb6c2 | |
Benjamin Sago | e3e776a1fa | |
Benjamin Sago | 1f4e58ee52 | |
Benjamin Sago | dbd11d3804 | |
Benjamin Sago | b1c49341c0 | |
Benjamin Sago | 550f2d29c2 | |
Benjamin Sago | 31043462af | |
Benjamin Sago | 311c9baf65 | |
Benjamin Sago | d19d66d57a | |
Benjamin Sago | 051a46c643 | |
ariasuni | bfd2644869 | |
Benjamin Sago | f6e66d982d | |
ariasuni | bd4f144884 | |
Benjamin Sago | 6403336028 | |
Benjamin Sago | e86f2b938b | |
Benjamin Sago | f3ae6a9bd9 | |
ariasuni | 8c10feec51 | |
ariasuni | f673e018b5 | |
ariasuni | 3a8005c840 | |
ariasuni | beaf561086 | |
ariasuni | a0457f9c87 | |
ariasuni | 050931e48f | |
ariasuni | c729e226da | |
Benjamin Sago | 2aaead1721 | |
ariasuni | 7a4cde75eb | |
skyline75489 | 0adc5c789b | |
Benjamin Sago | 505808fd9f | |
Benjamin Sago | 03cd3a81ff | |
Benjamin Sago | aee9c0e623 | |
Benjamin Sago | 8c34b46a22 | |
skyline75489 | 8ad46e2ee5 | |
Benjamin Sago | b051ac9477 | |
Tezkerek | 1da705c355 | |
Benjamin Sago | ba763d4bbd | |
Alexander Adam | 8f8e81182a | |
Benjamin Sago | 004589d079 | |
Benjamin Sago | 701a2e60e5 | |
Benjamin Sago | 83f245237b | |
Benjamin Sago | 622904d07c | |
Benjamin Sago | 4a108c1786 | |
Benjamin Sago | 5d13a65e4f | |
Benjamin Sago | c1435411c3 | |
b05902132 | 2f72b3ff3a | |
b05902132 | 53ccf18746 | |
b05902132 | 76f20f8b7d | |
b05902132 | 46a3e59e66 | |
Chester Liu | 0ea8f17b22 | |
Benjamin Sago | f27fb2ab7a | |
Benjamin Sago | 96e3f3d8d3 | |
Benjamin Sago | 6e0a080c15 | |
Benjamin Sago | 96e9307b99 | |
Chester Liu | 00f97a9738 | |
Chester Liu | 78a3bc9838 | |
Benjamin Sago | 3c69169993 | |
Benjamin Sago | e6edb888a0 | |
Benjamin Sago | 005a174e60 | |
Benjamin Sago | da171c405f | |
Benjamin Sago | cf5070a20e | |
Chester Liu | 9d613016c0 | |
Chester Liu | e874584a55 | |
Mélanie Chauvel | 435161e6e4 | |
ariasuni | 74ecf6d311 | |
Chester Liu | 33dd8fd2ca | |
Chester Liu | 13b3635407 | |
Chester Liu | 5503e4756e | |
Chester Liu | 31583691d5 | |
Chester Liu | aeb4a679e8 | |
Chester Liu | e9d0af0343 | |
Chester Liu | 0e8a4582d0 | |
Chester Liu | 6a642d0f32 | |
PatriotRossii | 98e1948cd2 | |
PatriotRossii | 4e5e2ce8f0 | |
PatriotRossii | 5743e6d8e3 | |
PatriotRossii | 77ad59347a | |
PatriotRossii | 57cf0f1b23 | |
b05902132 | 4ea79ee11d | |
Stuart Carnie | c946bab08e | |
Stuart Carnie | 765fcf64c9 | |
Stuart Carnie | c42e0b3945 | |
Kartavya Vashishtha | b417433644 | |
whonore | cfa00bf4b2 | |
whonore | c8ed69368e | |
whonore | 2708360fb5 | |
Mélanie Chauvel | 13b91cced4 | |
0xflotus | d11ac5d49b | |
Jakub Jirutka | a6ad5fa2cd | |
ariasuni | 0f414cf0b9 | |
ariasuni | 73e43c0700 | |
ariasuni | 79bac416f6 | |
0xflotus | 7b15f32da8 | |
ariasuni | 13c6c75d3a | |
ariasuni | 42bc34f339 | |
ariasuni | a740512014 | |
Benjamin Sago | 39c8c67bf6 | |
Benjamin Sago | bfcefd3f82 | |
Benjamin Sago | 51be9f4c43 | |
Benjamin Sago | c83359225b | |
Benjamin Sago | 976db01b3e | |
Benjamin Sago | 67a6cdd46a | |
Benjamin Sago | f1e3e7c7ff | |
Benjamin Sago | b05f18cae0 | |
Benjamin Sago | a1869f208e | |
Benjamin Sago | 800c73ff24 | |
Benjamin Sago | fd730e436c | |
Benjamin Sago | bf3d58aa80 | |
Benjamin Sago | 3419afa7cf | |
Benjamin Sago | f0c63b64ec | |
Benjamin Sago | db6288a9bb | |
Benjamin Sago | eb90b4d168 | |
Benjamin Sago | 85414b2a61 | |
Benjamin Sago | e73a8859d5 | |
Benjamin Sago | 63ddab6958 | |
Laurent Arnoud | bf883884ed | |
Benjamin Sago | 93bd052c70 | |
Benjamin Sago | 06c899cac0 | |
Benjamin Sago | d8b21a33b5 | |
Benjamin Sago | ef7bb43dbb | |
Benjamin Sago | 61c5df7c11 | |
Benjamin Sago | 86de17b788 | |
Benjamin Sago | 91f1541e85 | |
Benjamin Sago | f7a3df6229 | |
Sudhip Nashi | 42b9a5802e | |
Benjamin Sago | 5dfa5769c3 | |
Benjamin Sago | 6ef7dba160 | |
Benjamin Sago | 31a2eba2fe | |
Benjamin Sago | 6eb5cee634 | |
Benjamin Sago | 002080cde8 | |
Benjamin Sago | 3dc86c99ad | |
Benjamin Sago | f42957fab8 | |
Benjamin Sago | 80f9806bbc | |
Benjamin Sago | ecbe63bf1e | |
Benjamin Sago | 6f2d8cc26c | |
Benjamin Sago | ed59428cbc | |
Benjamin Sago | f8df02dae7 | |
Benjamin Sago | c3c39fee0a | |
Benjamin Sago | a25cfe765d | |
Benjamin Sago | f0c139ca68 | |
Benjamin Sago | 70a30ed683 | |
Benjamin Sago | 74d9f1402b | |
Benjamin Sago | 39c3f15602 | |
Benjamin Sago | 8046980806 | |
Benjamin Sago | 04e2d4c692 | |
Benjamin Sago | 3b209d1127 | |
Benjamin Sago | 5ca3548bb1 | |
Benjamin Sago | df81a24dae | |
Benjamin Sago | ee898bef8d | |
Benjamin Sago | 0550faec05 | |
Benjamin Sago | e95eb5e9fc | |
Benjamin Sago | e44858eb41 | |
Benjamin Sago | 7f9773b68e | |
Benjamin Sago | fd3a3d13b7 | |
Benjamin Sago | dee447213c | |
Benjamin Sago | c167046c34 | |
Benjamin Sago | 9240dbdef3 | |
Benjamin Sago | 6f20f195a7 | |
Benjamin Sago | 8b852cb201 | |
Benjamin Sago | ba5547b74b | |
Benjamin Sago | d03e39c367 | |
Benjamin Sago | 8c3748ca35 | |
Benjamin Sago | 1ee8fab7de | |
Benjamin Sago | 083cf147be | |
Benjamin Sago | dbbc4e465b | |
Benjamin Sago | 5cf22677e1 | |
Benjamin Sago | ea62aab37e | |
Benjamin Sago | cba2070924 | |
Benjamin Sago | 5555faf64c | |
Benjamin Sago | 12e45ac41c | |
Benjamin Sago | d901bb289c | |
Benjamin Sago | 1fe06a7682 | |
Benjamin Sago | 4b459631aa | |
Benjamin Sago | 35c5da8a79 | |
Jon Gjengset | c0f01310c4 | |
Jon Gjengset | f37aeebef0 | |
Zach Coyle | 3b1d770f8a | |
Sudhip Nashi | c17cf95f4c | |
imsofi | 0a7ffcd042 | |
Karey Higuera | 5be0286749 | |
Tercio Gaudencio Filho | 3fe35b571f | |
Karey Higuera | e8d69fc5e8 | |
Gokul Swaminathan | 43a4fcb27a | |
Diego Magdaleno | 4085010b36 | |
Alexis Bourget | f872027cfe | |
Lars Haalck | 16046d57de | |
dalisoft | cf5a9cbf29 | |
Alain Schlesser | 2ccc8fa093 | |
Dmitriy Olshevskiy | 45eade9a59 | |
ariasuni | dba3f37b0a | |
Igor Raits | 80875aac8f | |
Thomas Hurst | acb7c49abf | |
Thomas Hurst | e54e1f53c8 | |
Thomas Hurst | 86163ab298 | |
Thomas Hurst | d2d2e7325f | |
Thomas Hurst | bc830b9158 | |
Igor Raits | da1ac5139b | |
Daniel Bayley | f018e093c7 | |
Kat Marchán | 7f717c3af3 | |
ariasuni | 5a84953b4e | |
ariasuni | 046af5cdd1 | |
Benjamin Sago | 78ba0b8973 | |
Laurent Arnoud | bd655f9c20 | |
Benjamin Sago | db8c6e4627 | |
Benjamin Sago | 490d9680c2 | |
Benjamin Sago | df0d30e966 | |
Benjamin Sago | c46dfc8836 | |
Eric Nielsen | 501fbf7182 | |
Benjamin Sago | 12c562c93f | |
Benjamin Sago | 9da3a460e8 | |
Benjamin Sago | eaece8cc7a | |
Benjamin Sago | 6d8f6906a1 | |
Benjamin Sago | d31b655f64 | |
Benjamin Sago | 2382be7524 | |
Benjamin Sago | 0a3f730878 | |
Benjamin Sago | 352afd40c3 | |
Benjamin Sago | 416818fda2 | |
Benjamin Sago | d13cce7d94 | |
Benjamin Sago | 3d82fc7683 | |
Benjamin Sago | efad1c9e4e | |
Benjamin Sago | 64bd1b9a55 | |
Benjamin Sago | 8a711358ee | |
Benjamin Sago | 1f167dda41 | |
Benjamin Sago | 5521f2d4ab | |
Benjamin Sago | c5a1e8e103 | |
Benjamin Sago | 565a323661 | |
Benjamin Sago | 404ed20fd3 | |
Benjamin Sago | 69827e05f3 | |
Benjamin Sago | fbd001e8d2 | |
Benjamin Sago | c835188b3c | |
Benjamin Sago | 4dab6237f1 | |
Benjamin Sago | e198cac3a7 | |
Benjamin Sago | 8ccff86940 | |
Benjamin Sago | 44664bf757 | |
Benjamin Sago | 9c98b2614b | |
Benjamin Sago | 5683b5796e | |
Benjamin Sago | 5575c22eeb | |
Benjamin Sago | ffd3fe41bb | |
Zheng He Hu | 128fadd8bf | |
Orvar Segerström | e791cf4181 | |
Orvar Segerström | 5d4a09ff01 | |
Orvar Segerström | 250104c9da | |
ariasuni | a636d08f8b | |
Maarten de Vries | 3ef6195ffd | |
Akatsuki | a0da89584e | |
ecanja | 18aeb4a2f3 | |
ecanja | aded70c98e | |
jbranchaud | 5823069dbf | |
FliegendeWurst | bbd6db3687 | |
FliegendeWurst | 6010ed5213 | |
FliegendeWurst | b5b731071c | |
ariasuni | efbd46351c | |
Alexandru-Sergiu Marton | f6071e9c46 | |
Martin Sehnoutka | a7a8e99cf3 | |
Eric Nielsen | cb933a6f62 | |
Vicente Reyes | 69a7e53ee3 | |
Orvar Segerström | 925f5173c1 | |
Laurent Arnoud | 3247de3cbb | |
Grigorii Horos | f4f83f4d37 | |
Grigorii Horos | f9540ead5b | |
Grigorii Horos | e45fb7a0a7 | |
Grigorii Horos | 0d3023657b | |
Brennan McDonald | 215b779d35 | |
Brennan McDonald | f19f7be726 | |
James Tai | dd9dffff8c | |
Steven Davies | a78cb0bef6 | |
Igor Gnatenko | 2fd76aa166 | |
Benjamin Sago | f73eae117d | |
Bond_009 | 8b60285320 | |
Bond_009 | f599c7ce93 | |
Benjamin Sago | 89c861f24d | |
Benjamin Sago | 4d9c6eb39c | |
Oleksii Filonenko | 58c821be6e | |
Benjamin Sago | 4c73a33530 | |
Michael Hackner | e3fc2de088 | |
ariasuni | 3ee0a6afe9 | |
E.M. Gelblicht | 1bf9e397e5 | |
Mahmoud Al-Qudsi | ad02241ac2 | |
Mahmoud Al-Qudsi | 36cf5df044 | |
Will DeBerry | dfa7e7bab6 | |
Alexandre Erwin Ittner | 75a88823d9 |
|
@ -0,0 +1 @@
|
|||
github: ogham
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: exa is unmaintained
|
||||
about: Please use the active fork eza instead. <https://github.com/eza-community/eza>
|
||||
---
|
||||
|
||||
exa is unmaintained, please use the active fork eza instead. <https://github.com/eza-community/eza>
|
||||
|
||||
---
|
|
@ -0,0 +1,46 @@
|
|||
name: Unit tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/*'
|
||||
- 'src/**'
|
||||
- 'Cargo.*'
|
||||
- build.rs
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/*'
|
||||
- 'src/**'
|
||||
- 'Cargo.*'
|
||||
- build.rs
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
continue-on-error: ${{ matrix.rust == 'nightly' }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
rust: [1.66.1, stable, beta, nightly]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
|
||||
- name: Install cargo-hack
|
||||
run: cargo install cargo-hack@0.5.27
|
||||
|
||||
- name: Run unit tests
|
||||
run: cargo hack test --feature-powerset
|
|
@ -3,7 +3,7 @@ target
|
|||
|
||||
# Vagrant stuff
|
||||
.vagrant
|
||||
ubuntu-xenial-16.04-cloudimg-console.log
|
||||
*.log
|
||||
|
||||
# Compiled artifacts
|
||||
# (see devtools/*-package-for-*.sh)
|
||||
|
@ -13,3 +13,9 @@ ubuntu-xenial-16.04-cloudimg-console.log
|
|||
/exa-macos-x86_64-*.zip
|
||||
/MD5SUMS
|
||||
/SHA1SUMS
|
||||
|
||||
# Snap stuff
|
||||
parts
|
||||
prime
|
||||
stage
|
||||
*.snap
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
disable_all_formatting = true
|
13
.travis.yml
13
.travis.yml
|
@ -1,13 +0,0 @@
|
|||
before_install:
|
||||
- sudo add-apt-repository --yes ppa:kubuntu-ppa/backports
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install cmake
|
||||
sudo: true
|
||||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
script:
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
- cargo build --verbose --no-default-features
|
||||
- cargo test --verbose --no-default-features
|
|
@ -1,555 +1,396 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
version = 3
|
||||
|
||||
[[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.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "0.1.4"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.0.4"
|
||||
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.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "datetime"
|
||||
version = "0.4.7"
|
||||
version = "1.0.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
|
||||
dependencies = [
|
||||
"iso8601 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pad 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"jobserver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.6.1"
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "datetime"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c3f7a77f3e57fedf80e09136f2d8777ebf621207306f6d96d610af048354bc"
|
||||
dependencies = [
|
||||
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc",
|
||||
"locale",
|
||||
"pad",
|
||||
"redox_syscall",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exa"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"ansi_term 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"datetime 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"git2 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"term_grid 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"users 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"zoneinfo_compiled 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ansi_term",
|
||||
"datetime",
|
||||
"git2",
|
||||
"glob",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"locale",
|
||||
"log",
|
||||
"natord",
|
||||
"num_cpus",
|
||||
"number_prefix",
|
||||
"scoped_threadpool",
|
||||
"term_grid",
|
||||
"terminal_size",
|
||||
"unicode-width",
|
||||
"users",
|
||||
"zoneinfo_compiled",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.9.1"
|
||||
version = "0.13.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9831e983241f8c5591ed53f17d874833e2fa82cac2625f3888c50cbfe136cba"
|
||||
dependencies = [
|
||||
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libgit2-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"openssl-sys 0.9.47 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"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.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
|
||||
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.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||
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.1.1"
|
||||
name = "jobserver"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd"
|
||||
dependencies = [
|
||||
"nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"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.51"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.8.1"
|
||||
version = "0.12.21+1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86271bacd72b2b9e854c3dcfb82efd538f15f870e4c11af66900effb462f6825"
|
||||
dependencies = [
|
||||
"cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"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.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"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.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.6"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.7 (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"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
|
||||
|
||||
[[package]]
|
||||
name = "natord"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.10.0"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
|
||||
dependencies = [
|
||||
"libc 0.2.51 (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.15.0+1.1.1k"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1a5f6ae2ac04393b217ea9f700cd04fa9bf3d93fae2872069f3d15d908af70a"
|
||||
dependencies = [
|
||||
"cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.47"
|
||||
version = "0.9.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f"
|
||||
dependencies = [
|
||||
"autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"openssl-src 111.3.0+1.1.1c (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"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.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.54"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
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.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
|
||||
|
||||
[[package]]
|
||||
name = "term_grid"
|
||||
version = "0.1.7"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7c9eb7705cb3f0fd71d3955b23db6d372142ac139e8c473952c93bf3c3dc4b7"
|
||||
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"
|
||||
name = "terminal_size"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.0.4"
|
||||
name = "tinyvec"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342"
|
||||
dependencies = [
|
||||
"wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ucd-util"
|
||||
version = "0.1.3"
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0"
|
||||
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.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
|
||||
dependencies = [
|
||||
"smallvec 0.6.9 (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.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b"
|
||||
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)",
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"matches",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "users"
|
||||
version = "0.9.1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
|
||||
dependencies = [
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-ranges"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.6"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.7"
|
||||
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.7 (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.7 (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.4.8"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64fbebe65e899530f43bd760b23fda8f141118f4db49952b02998cbd0907a5de"
|
||||
dependencies = [
|
||||
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"datetime 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"byteorder",
|
||||
"datetime",
|
||||
]
|
||||
|
||||
[metadata]
|
||||
"checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c"
|
||||
"checksum ansi_term 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eaa72766c3585a1f812a3387a7e2c6cab780f899c2f43ff6ea06c8d071fcbb36"
|
||||
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
|
||||
"checksum autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0e49efa51329a5fd37e7c79db4621af617cd4e3e5bc224939808d076077077bf"
|
||||
"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
|
||||
"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
|
||||
"checksum cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)" = "5e5f3fee5eeb60324c2781f1e41286bdee933850fff9b3c672587fed5ec58c83"
|
||||
"checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4"
|
||||
"checksum datetime 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5c44b6c112860e38412e0c4732172d723458d40db906ee4b9ce87544f022a7b9"
|
||||
"checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a"
|
||||
"checksum git2 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "924b2e7d2986e625dcad89e8a429a7b3adee3c3d71e585f4a66c4f7e78715e31"
|
||||
"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
|
||||
"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
|
||||
"checksum iso8601 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "11dc464f8c6f17595d191447c9c6559298b2d023d6f846a4a23ac7ea3c46c477"
|
||||
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||
"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
|
||||
"checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917"
|
||||
"checksum libgit2-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "941a41e23f77323b8c9d2ee118aec9ee39dfc176078c18b4757d3bad049d9ff7"
|
||||
"checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe"
|
||||
"checksum locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd"
|
||||
"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
|
||||
"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
|
||||
"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
|
||||
"checksum natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
|
||||
"checksum nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
|
||||
"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
|
||||
"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1"
|
||||
"checksum num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a23f0ed30a54abaa0c7e83b1d2d87ada7c3c23078d1d87815af3e3b6385fbba"
|
||||
"checksum number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
|
||||
"checksum openssl-src 111.3.0+1.1.1c (registry+https://github.com/rust-lang/crates.io-index)" = "53ed5f31d294bdf5f7a4ba0a206c2754b0f60e9a63b7e3076babc5317873c797"
|
||||
"checksum openssl-sys 0.9.47 (registry+https://github.com/rust-lang/crates.io-index)" = "75bdd6dbbb4958d38e47a1d2348847ad1eb4dc205dc5d37473ae504391865acc"
|
||||
"checksum pad 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9b8de33465981073e32e1d75bb89ade49062bb853e7c97ec2c13439095563a"
|
||||
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
|
||||
"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
|
||||
"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
|
||||
"checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252"
|
||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||
"checksum regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8f0a0bcab2fd7d1d7c54fa9eae6f43eddeb9ce2e7352f8518a814a4f65d60c58"
|
||||
"checksum regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dcfd8681eebe297b81d98498869d4aae052137651ad7b96822f09ceb690d0a96"
|
||||
"checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
|
||||
"checksum smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c4488ae950c49d403731982257768f48fada354a5203fe81f9bb6f43ca9002be"
|
||||
"checksum term_grid 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "230d3e804faaed5a39b08319efb797783df2fd9671b39b7596490cb486d702cf"
|
||||
"checksum term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9e5b9a66db815dcfd2da92db471106457082577c3c278d4138ab3e3b4e189327"
|
||||
"checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f"
|
||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
|
||||
"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
|
||||
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
|
||||
"checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426"
|
||||
"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
|
||||
"checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
|
||||
"checksum users 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c72f4267aea0c3ec6d07eaabea6ead7c5ddacfafc5e22bcf8d186706851fb4cf"
|
||||
"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
|
||||
"checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d"
|
||||
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||
"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770"
|
||||
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
|
||||
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba"
|
||||
"checksum zoneinfo_compiled 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6e136f8e8905dcf086773dbb987be12b149e240bf4039dce9d068774780ad52e"
|
||||
|
|
98
Cargo.toml
98
Cargo.toml
|
@ -1,65 +1,83 @@
|
|||
[package]
|
||||
name = "exa"
|
||||
version = "0.9.0"
|
||||
authors = [ "Benjamin Sago <ogham@bsago.me>" ]
|
||||
build = "build.rs"
|
||||
|
||||
description = "A modern replacement for ls"
|
||||
homepage = "https://the.exa.website/"
|
||||
repository = "https://github.com/ogham/exa"
|
||||
documentation = "https://github.com/ogham/exa"
|
||||
|
||||
readme = "README.md"
|
||||
authors = ["Benjamin Sago <ogham@bsago.me>"]
|
||||
categories = ["command-line-utilities"]
|
||||
keywords = ["ls", "files", "command-line"]
|
||||
edition = "2021"
|
||||
rust-version = "1.66.1"
|
||||
exclude = ["/devtools/*", "/Justfile", "/Vagrantfile", "/screenshots.png"]
|
||||
readme = "README.md"
|
||||
homepage = "https://the.exa.website/"
|
||||
license = "MIT"
|
||||
exclude = ["/devtools/*", "/Makefile", "/Vagrantfile", "/screenshots.png"]
|
||||
repository = "https://github.com/ogham/exa"
|
||||
version = "0.10.1"
|
||||
|
||||
|
||||
[[bin]]
|
||||
name = "exa"
|
||||
path = "src/bin/main.rs"
|
||||
doc = false
|
||||
|
||||
|
||||
[lib]
|
||||
name = "exa"
|
||||
path = "src/exa.rs"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
datetime = "0.4.7"
|
||||
env_logger = "0.6.1"
|
||||
glob = "0.3.0"
|
||||
lazy_static = "1.3.0"
|
||||
libc = "0.2.51"
|
||||
locale = "0.2.2"
|
||||
log = "0.4.6"
|
||||
natord = "1.0.9"
|
||||
num_cpus = "1.10.0"
|
||||
number_prefix = "0.3.0"
|
||||
scoped_threadpool = "0.1.9"
|
||||
term_grid = "0.1.7"
|
||||
term_size = "0.3.1"
|
||||
unicode-width = "0.1.5"
|
||||
users = "0.9.1"
|
||||
zoneinfo_compiled = "0.4.8"
|
||||
ansi_term = "0.12"
|
||||
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.2.0"
|
||||
terminal_size = "0.1.16"
|
||||
unicode-width = "0.1"
|
||||
zoneinfo_compiled = "0.5.1"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
users = "0.11"
|
||||
|
||||
[dependencies.datetime]
|
||||
version = "0.5.2"
|
||||
default-features = false
|
||||
features = ["format"]
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.9.1"
|
||||
version = "0.13"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[build-dependencies]
|
||||
datetime = "0.4.7"
|
||||
[build-dependencies.datetime]
|
||||
version = "0.5.2"
|
||||
default-features = false
|
||||
|
||||
[features]
|
||||
default = [ "git" ]
|
||||
git = [ "git2" ]
|
||||
vendored-openssl = ["git2/vendored-openssl"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
||||
# make dev builds faster by excluding debug symbols
|
||||
[profile.dev]
|
||||
debug = false
|
||||
|
||||
# use LTO for smaller binaries (that take longer to build)
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
|
||||
|
||||
[package.metadata.deb]
|
||||
license-file = [ "LICENCE", "4" ]
|
||||
depends = "$auto"
|
||||
extended-description = """
|
||||
exa is a replacement for ls written in Rust.
|
||||
"""
|
||||
section = "utils"
|
||||
priority = "optional"
|
||||
assets = [
|
||||
[ "target/release/exa", "/usr/bin/exa", "0755" ],
|
||||
[ "target/release/../man/exa.1", "/usr/share/man/man1/exa.1", "0644" ],
|
||||
[ "target/release/../man/exa_colors.5", "/usr/share/man/man5/exa_colors.5", "0644" ],
|
||||
[ "completions/bash/exa", "/usr/share/bash-completion/completions/exa", "0644" ],
|
||||
[ "completions/zsh/_exa", "/usr/share/zsh/site-functions/_exa", "0644" ],
|
||||
[ "completions/fish/exa.fish", "/usr/share/fish/vendor_completions.d/exa.fish", "0644" ],
|
||||
]
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
all: build test
|
||||
all-release: build-release test-release
|
||||
|
||||
|
||||
#----------#
|
||||
# building #
|
||||
#----------#
|
||||
|
||||
# compile the exa binary
|
||||
@build:
|
||||
cargo build
|
||||
|
||||
# compile the exa binary (in release mode)
|
||||
@build-release:
|
||||
cargo build --release --verbose
|
||||
|
||||
# produce an HTML chart of compilation timings
|
||||
@build-time:
|
||||
cargo +nightly clean
|
||||
cargo +nightly build -Z timings
|
||||
|
||||
# check that the exa binary can compile
|
||||
@check:
|
||||
cargo check
|
||||
|
||||
|
||||
#---------------#
|
||||
# running tests #
|
||||
#---------------#
|
||||
|
||||
# run unit tests
|
||||
@test:
|
||||
cargo test --workspace -- --quiet
|
||||
|
||||
# run unit tests (in release mode)
|
||||
@test-release:
|
||||
cargo test --workspace --release --verbose
|
||||
|
||||
|
||||
#------------------------#
|
||||
# running extended tests #
|
||||
#------------------------#
|
||||
|
||||
# run extended tests
|
||||
@xtests:
|
||||
xtests/run.sh
|
||||
|
||||
# run extended tests (using the release mode exa)
|
||||
@xtests-release:
|
||||
xtests/run.sh --release
|
||||
|
||||
# display the number of extended tests that get run
|
||||
@count-xtests:
|
||||
grep -F '[[cmd]]' -R xtests | wc -l
|
||||
|
||||
|
||||
#-----------------------#
|
||||
# code quality and misc #
|
||||
#-----------------------#
|
||||
|
||||
# lint the code
|
||||
@clippy:
|
||||
touch src/main.rs
|
||||
cargo clippy
|
||||
|
||||
# update dependency versions, and checks for outdated ones
|
||||
@update-deps:
|
||||
cargo update
|
||||
command -v cargo-outdated >/dev/null || (echo "cargo-outdated not installed" && exit 1)
|
||||
cargo outdated
|
||||
|
||||
# list unused dependencies
|
||||
@unused-deps:
|
||||
command -v cargo-udeps >/dev/null || (echo "cargo-udeps not installed" && exit 1)
|
||||
cargo +nightly udeps
|
||||
|
||||
# check that every combination of feature flags is successful
|
||||
@check-features:
|
||||
command -v cargo-hack >/dev/null || (echo "cargo-hack not installed" && exit 1)
|
||||
cargo hack check --feature-powerset
|
||||
|
||||
# build exa and run extended tests with features disabled
|
||||
@feature-checks *args:
|
||||
cargo build --no-default-features
|
||||
specsheet xtests/features/none.toml -shide {{args}} \
|
||||
-O cmd.target.exa="${CARGO_TARGET_DIR:-../../target}/debug/exa"
|
||||
|
||||
# print versions of the necessary build tools
|
||||
@versions:
|
||||
rustc --version
|
||||
cargo --version
|
||||
|
||||
|
||||
#---------------#
|
||||
# documentation #
|
||||
#---------------#
|
||||
|
||||
# build the man pages
|
||||
@man:
|
||||
mkdir -p "${CARGO_TARGET_DIR:-target}/man"
|
||||
pandoc --standalone -f markdown -t man man/exa.1.md > "${CARGO_TARGET_DIR:-target}/man/exa.1"
|
||||
pandoc --standalone -f markdown -t man man/exa_colors.5.md > "${CARGO_TARGET_DIR:-target}/man/exa_colors.5"
|
||||
|
||||
# build and preview the main man page (exa.1)
|
||||
@man-1-preview: man
|
||||
man "${CARGO_TARGET_DIR:-target}/man/exa.1"
|
||||
|
||||
# build and preview the colour configuration man page (exa_colors.5)
|
||||
@man-5-preview: man
|
||||
man "${CARGO_TARGET_DIR:-target}/man/exa_colors.5"
|
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
|
228
README.md
228
README.md
|
@ -1,23 +1,43 @@
|
|||
# exa [![Build status](https://travis-ci.org/ogham/exa.svg)](https://travis-ci.org/ogham/exa)
|
||||
# exa is unmaintained, use the [fork eza](https://github.com/eza-community/eza) instead.
|
||||
|
||||
[exa](https://the.exa.website/) is a replacement for `ls` written in Rust.
|
||||
(This repository isn’t archived because the only person with the rights to do so is unreachable).
|
||||
|
||||
## Rationale
|
||||
---
|
||||
|
||||
**exa** is a modern replacement for the command-line program ls that ships with Unix and Linux operating systems, with more features and better defaults. It uses colours to distinguish file types and metadata. It knows about symlinks, extended attributes, and Git. And it’s **small**, **fast**, and just one **single binary**.
|
||||
<div align="center">
|
||||
|
||||
By deliberately making some decisions differently, exa attempts to be a more featureful, more user-friendly version of ls.
|
||||
# exa
|
||||
|
||||
## Screenshots
|
||||
[exa](https://the.exa.website/) is a modern replacement for _ls_.
|
||||
|
||||
**README Sections:** [Options](#options) — [Installation](#installation) — [Development](#development)
|
||||
|
||||
[![Unit tests](https://github.com/ogham/exa/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/ogham/exa/actions/workflows/unit-tests.yml)
|
||||
[![Say thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)]()
|
||||
</div>
|
||||
|
||||
![Screenshots of exa](screenshots.png)
|
||||
|
||||
---
|
||||
|
||||
## Options
|
||||
**exa** is a modern replacement for the venerable file-listing command-line program `ls` that ships with Unix and Linux operating systems, giving it more features and better defaults.
|
||||
It uses colours to distinguish file types and metadata.
|
||||
It knows about symlinks, extended attributes, and Git.
|
||||
And it’s **small**, **fast**, and just **one single binary**.
|
||||
|
||||
exa’s options are almost, but not quite, entirely unlike `ls`'s.
|
||||
By deliberately making some decisions differently, exa attempts to be a more featureful, more user-friendly version of `ls`.
|
||||
For more information, see [exa’s website](https://the.exa.website/).
|
||||
|
||||
### Display Options
|
||||
|
||||
---
|
||||
|
||||
<a id="options">
|
||||
<h1>Command-line options</h1>
|
||||
</a>
|
||||
|
||||
exa’s options are almost, but not quite, entirely unlike `ls`’s.
|
||||
|
||||
### Display options
|
||||
|
||||
- **-1**, **--oneline**: display one entry per line
|
||||
- **-G**, **--grid**: display entries as a grid (default)
|
||||
|
@ -25,10 +45,13 @@ exa’s options are almost, but not quite, entirely unlike `ls`'s.
|
|||
- **-R**, **--recurse**: recurse into directories
|
||||
- **-T**, **--tree**: recurse into directories as a tree
|
||||
- **-x**, **--across**: sort the grid across, rather than downwards
|
||||
- **-F**, **--classify**: display type indicator by file names
|
||||
- **--colo[u]r**: when to use terminal colours
|
||||
- **--colo[u]r-scale**: highlight levels of file sizes distinctly
|
||||
- **--icons**: display icons
|
||||
- **--no-icons**: don't display icons (always overrides --icons)
|
||||
|
||||
### Filtering Options
|
||||
### Filtering options
|
||||
|
||||
- **-a**, **--all**: show hidden and 'dot' files
|
||||
- **-d**, **--list-dirs**: list directories like regular files
|
||||
|
@ -42,24 +65,32 @@ exa’s options are almost, but not quite, entirely unlike `ls`'s.
|
|||
|
||||
Pass the `--all` option twice to also show the `.` and `..` directories.
|
||||
|
||||
### Long View Options
|
||||
### Long view options
|
||||
|
||||
These options are available when running with --long (`-l`):
|
||||
These options are available when running with `--long` (`-l`):
|
||||
|
||||
- **-b**, **--binary**: list file sizes with binary prefixes
|
||||
- **-B**, **--bytes**: list file sizes in bytes, without any prefixes
|
||||
- **-g**, **--group**: list each file's group
|
||||
- **-g**, **--group**: list each file’s group
|
||||
- **-h**, **--header**: add a header row to each column
|
||||
- **-H**, **--links**: list each file's number of hard links
|
||||
- **-i**, **--inode**: list each file's inode number
|
||||
- **-H**, **--links**: list each file’s number of hard links
|
||||
- **-i**, **--inode**: list each file’s inode number
|
||||
- **-m**, **--modified**: use the modified timestamp field
|
||||
- **-S**, **--blocks**: list each file's number of file system blocks
|
||||
- **-S**, **--blocks**: list each file’s number of file system blocks
|
||||
- **-t**, **--time=(field)**: which timestamp field to use
|
||||
- **-u**, **--accessed**: use the accessed timestamp field
|
||||
- **-U**, **--created**: use the created timestamp field
|
||||
- **-@**, **--extended**: list each file's extended attributes and sizes
|
||||
- **--git**: list each file's Git status, if tracked or ignored
|
||||
- **-@**, **--extended**: list each file’s extended attributes and sizes
|
||||
- **--changed**: use the changed timestamp field
|
||||
- **--git**: list each file’s Git status, if tracked or ignored
|
||||
- **--time-style**: how to format timestamps
|
||||
- **--no-permissions**: suppress the permissions field
|
||||
- **--octal-permissions**: list each file's permission in octal format
|
||||
- **--no-filesize**: suppress the filesize field
|
||||
- **--no-user**: suppress the user field
|
||||
- **--no-time**: suppress the time field
|
||||
|
||||
Some of the options accept parameters:
|
||||
|
||||
- Valid **--color** options are **always**, **automatic**, and **never**.
|
||||
- Valid sort fields are **accessed**, **changed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase. The modified field has the aliases **date**, **time**, and **newest**, while its reverse has the aliases **age** and **oldest**.
|
||||
|
@ -67,55 +98,164 @@ These options are available when running with --long (`-l`):
|
|||
- Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**.
|
||||
|
||||
|
||||
## Installation
|
||||
---
|
||||
|
||||
exa is written in [Rust](http://www.rust-lang.org). You will need rustc version 1.17.0 or higher. The recommended way to install Rust is from the official download page.
|
||||
Once you have it set up, a simple `make install` will compile exa and install it into `/usr/local/bin`.
|
||||
<a id="installation">
|
||||
<h1>Installation</h1>
|
||||
</a>
|
||||
|
||||
exa depends on [libgit2](https://github.com/alexcrichton/git2-rs) for certain features.
|
||||
If you’re unable to compile libgit2, you can opt out of Git support by running `cargo build --release --no-default-features`.
|
||||
exa is available for macOS and Linux.
|
||||
More information on how to install exa is available on [the Installation page](https://the.exa.website/install).
|
||||
|
||||
If you intend to compile for musl you will need to use the flag vendored-openssl if you want to get the Git feature working: `cargo build --release --target=x86_64-unknown-linux-musl --features vendored-openssl,git`
|
||||
### Alpine Linux
|
||||
|
||||
### Cargo Install
|
||||
On Alpine Linux, [enable community repository](https://wiki.alpinelinux.org/wiki/Enable_Community_Repository) and install the [`exa`](https://pkgs.alpinelinux.org/package/edge/community/x86_64/exa) package.
|
||||
|
||||
If you’re using a recent version of Cargo (0.5.0 or higher), you can use the `cargo install` command:
|
||||
apk add exa
|
||||
|
||||
cargo install exa
|
||||
### Arch Linux
|
||||
|
||||
or:
|
||||
On Arch, install the [`exa`](https://www.archlinux.org/packages/community/x86_64/exa/) package.
|
||||
|
||||
cargo install --no-default-features exa
|
||||
pacman -S exa
|
||||
|
||||
Cargo will build the `exa` binary and place it in `$HOME/.cargo` (this location can be overridden by setting the `--root` option).
|
||||
### Android / Termux
|
||||
|
||||
On Android / Termux, install the [`exa`](https://github.com/termux/termux-packages/tree/master/packages/exa) package.
|
||||
|
||||
pkg install exa
|
||||
|
||||
### Debian
|
||||
|
||||
On Debian, install the [`exa`](https://packages.debian.org/stable/exa) package.
|
||||
|
||||
apt install exa
|
||||
|
||||
### Fedora
|
||||
|
||||
On Fedora, install the [`exa`](https://src.fedoraproject.org/modules/exa) package.
|
||||
|
||||
dnf install exa
|
||||
|
||||
### Gentoo
|
||||
|
||||
On Gentoo, install the [`sys-apps/exa`](https://packages.gentoo.org/packages/sys-apps/exa) package.
|
||||
|
||||
emerge sys-apps/exa
|
||||
|
||||
### Homebrew
|
||||
|
||||
If you're using [homebrew](https://brew.sh/), you can use the `brew install` command:
|
||||
If you’re using [Homebrew](https://brew.sh/) on macOS, install the [`exa`](http://formulae.brew.sh/formula/exa) formula.
|
||||
|
||||
brew install exa
|
||||
|
||||
or:
|
||||
### MacPorts
|
||||
|
||||
brew install exa --without-git
|
||||
If you're using [MacPorts](https://www.macports.org/) on macOS, install the [`exa`](https://ports.macports.org/port/exa/summary) port.
|
||||
|
||||
[Formulae](https://github.com/Homebrew/homebrew-core/blob/master/Formula/exa.rb)
|
||||
port install exa
|
||||
|
||||
### Nix
|
||||
|
||||
`exa` is also installable through [the derivation](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/exa/default.nix) using the [nix package manager](https://nixos.org/nix/) by running:
|
||||
On nixOS, install the [`exa`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/exa/default.nix) package.
|
||||
|
||||
nix-env -i exa
|
||||
|
||||
## Testing with Vagrant
|
||||
### openSUSE
|
||||
|
||||
On openSUSE, install the [`exa`](https://software.opensuse.org/package/exa) package.
|
||||
|
||||
zypper install exa
|
||||
|
||||
### Ubuntu
|
||||
|
||||
On Ubuntu 20.10 (Groovy Gorilla) and later, install the [`exa`](https://packages.ubuntu.com/jammy/exa) package.
|
||||
|
||||
sudo apt install exa
|
||||
|
||||
### Void Linux
|
||||
|
||||
On Void Linux, install the [`exa`](https://github.com/void-linux/void-packages/blob/master/srcpkgs/exa/template) package.
|
||||
|
||||
xbps-install -S exa
|
||||
|
||||
### Manual installation from GitHub
|
||||
|
||||
Compiled binary versions of exa are uploaded to GitHub when a release is made.
|
||||
You can install exa manually by [downloading a release](https://github.com/ogham/exa/releases), extracting it, and copying the binary to a directory in your `$PATH`, such as `/usr/local/bin`.
|
||||
|
||||
For more information, see the [Manual Installation page](https://the.exa.website/install/linux#manual).
|
||||
|
||||
### Cargo
|
||||
|
||||
If you already have a Rust environment set up, you can use the `cargo install` command:
|
||||
|
||||
cargo install exa
|
||||
|
||||
Cargo will build the `exa` binary and place it in `$HOME/.cargo`.
|
||||
|
||||
To build without Git support, run `cargo install --no-default-features exa` is also available, if the requisite dependencies are not installed.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<a id="development">
|
||||
<h1>Development
|
||||
|
||||
<a href="https://blog.rust-lang.org/2023/01/10/Rust-1.66.1.html">
|
||||
<img src="https://img.shields.io/badge/rustc-1.66.1+-lightgray.svg" alt="Rust 1.66.1+" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/ogham/exa/blob/master/LICENCE">
|
||||
<img src="https://img.shields.io/badge/licence-MIT-green" alt="MIT Licence" />
|
||||
</a>
|
||||
</h1></a>
|
||||
|
||||
exa is written in [Rust](https://www.rust-lang.org/).
|
||||
You will need rustc version 1.66.1 or higher.
|
||||
The recommended way to install Rust for development is from the [official download page](https://www.rust-lang.org/tools/install), using rustup.
|
||||
|
||||
Once Rust is installed, you can compile exa with Cargo:
|
||||
|
||||
cargo build
|
||||
cargo test
|
||||
|
||||
- The [just](https://github.com/casey/just) command runner can be used to run some helpful development commands, in a manner similar to `make`.
|
||||
Run `just --list` to get an overview of what’s available.
|
||||
|
||||
- If you are compiling a copy for yourself, be sure to run `cargo build --release` or `just build-release` to benefit from release-mode optimisations.
|
||||
Copy the resulting binary, which will be in the `target/release` directory, into a folder in your `$PATH`.
|
||||
`/usr/local/bin` is usually a good choice.
|
||||
|
||||
- To compile and install the manual pages, you will need [pandoc](https://pandoc.org/).
|
||||
The `just man` command will compile the Markdown into manual pages, which it will place in the `target/man` directory.
|
||||
To use them, copy them into a directory that `man` will read.
|
||||
`/usr/local/share/man` is usually a good choice.
|
||||
|
||||
- exa depends on [libgit2](https://github.com/rust-lang/git2-rs) for certain features.
|
||||
If you’re unable to compile libgit2, you can opt out of Git support by running `cargo build --no-default-features`.
|
||||
|
||||
- If you intend to compile for musl, you will need to use the flag `vendored-openssl` if you want to get the Git feature working.
|
||||
The full command is `cargo build --release --target=x86_64-unknown-linux-musl --features vendored-openssl,git`.
|
||||
|
||||
For more information, see the [Building from Source page](https://the.exa.website/install/source).
|
||||
|
||||
|
||||
### Testing with Vagrant
|
||||
|
||||
exa uses [Vagrant][] to configure virtual machines for testing.
|
||||
|
||||
Programs such as exa that are basically interfaces to the system are [notoriously difficult to test][testing]. Although the internal components have unit tests, it’s impossible to do a complete end-to-end test without mandating the current user’s name, the time zone, the locale, and directory structure to test. (And yes, these tests are worth doing. I have missed an edge case on more than one occasion.)
|
||||
Programs such as exa that are basically interfaces to the system are [notoriously difficult to test][testing].
|
||||
Although the internal components have unit tests, it’s impossible to do a complete end-to-end test without mandating the current user’s name, the time zone, the locale, and directory structure to test.
|
||||
(And yes, these tests are worth doing. I have missed an edge case on many an occasion.)
|
||||
|
||||
The initial attempt to solve the problem was just to create a directory of “awkward” test cases, run exa on it, and make sure it produced the correct output. But even this output would change if, say, the user’s locale formats dates in a different way. These can be mocked inside the code, but at the cost of making that code more complicated to read and understand.
|
||||
The initial attempt to solve the problem was just to create a directory of “awkward” test cases, run exa on it, and make sure it produced the correct output.
|
||||
But even this output would change if, say, the user’s locale formats dates in a different way.
|
||||
These can be mocked inside the code, but at the cost of making that code more complicated to read and understand.
|
||||
|
||||
An alternative solution is to fake *everything*: create a virtual machine with a known state and run the tests on *that*. This is what Vagrant does. Although it takes a while to download and set up, it gives everyone the same development environment to test for any obvious regressions.
|
||||
An alternative solution is to fake *everything*: create a virtual machine with a known state and run the tests on *that*.
|
||||
This is what Vagrant does.
|
||||
Although it takes a while to download and set up, it gives everyone the same development environment to test for any obvious regressions.
|
||||
|
||||
[Vagrant]: https://www.vagrantup.com/
|
||||
[testing]: https://eev.ee/blog/2016/08/22/testing-for-people-who-hate-testing/#troublesome-cases
|
||||
|
@ -124,7 +264,8 @@ First, initialise the VM:
|
|||
|
||||
host$ vagrant up
|
||||
|
||||
The first command downloads the virtual machine image, and then runs our provisioning script, which installs Rust, exa’s dependencies, configures the environment, and generates some awkward files and folders to use as test cases. This takes some time, but it does write to output occasionally. Once this is done, you can SSH in, and build and test:
|
||||
The first command downloads the virtual machine image, and then runs our provisioning script, which installs Rust and exa’s build-time dependencies, configures the environment, and generates some awkward files and folders to use as test cases.
|
||||
Once this is done, you can SSH in, and build and test:
|
||||
|
||||
host$ vagrant ssh
|
||||
vm$ cd /vagrant
|
||||
|
@ -132,7 +273,6 @@ The first command downloads the virtual machine image, and then runs our provisi
|
|||
vm$ ./xtests/run
|
||||
All the tests passed!
|
||||
|
||||
|
||||
### Running without Vagrant
|
||||
|
||||
Of course, the drawback of having a standard development environment is that you stop noticing bugs that occur outside of it. For this reason, Vagrant isn’t a *necessary* development step — it’s there if you’d like to use it, but exa still gets used and tested on other platforms. It can still be built and compiled on any target triple that it supports, VM or no VM, with `cargo build` and `cargo test`.
|
||||
Of course, the drawback of having a standard development environment is that you stop noticing bugs that occur outside of it.
|
||||
For this reason, Vagrant isn’t a *necessary* development step — it’s there if you’d like to use it, but exa still gets used and tested on other platforms.
|
||||
It can still be built and compiled on any target triple that it supports, VM or no VM, with `cargo build` and `cargo test`.
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
require 'date'
|
||||
|
||||
Vagrant.configure(2) do |config|
|
||||
|
||||
# We use Ubuntu instead of Debian because the image comes with two-way
|
||||
# shared folder support by default.
|
||||
UBUNTU = 'ubuntu/xenial64'
|
||||
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 = 1024
|
||||
v.cpus = 1
|
||||
v.memory = 2048
|
||||
v.cpus = `nproc`.chomp.to_i
|
||||
end
|
||||
|
||||
config.vm.provider :vmware_desktop do |v|
|
||||
v.vmx['memsize'] = '2048'
|
||||
v.vmx['numvcpus'] = `nproc`.chomp
|
||||
end
|
||||
|
||||
config.vm.box = UBUNTU
|
||||
|
@ -27,20 +29,19 @@ Vagrant.configure(2) do |config|
|
|||
# Install the dependencies needed for exa to build, as quietly as
|
||||
# apt can do.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
apt-get update
|
||||
apt-get install -qq -o=Dpkg::Use-Pty=0 -y \
|
||||
git cmake curl attr libgit2-dev zip \
|
||||
fish zsh bash bash-completion
|
||||
if hash fish &>/dev/null; then
|
||||
echo "Tools are already installed"
|
||||
else
|
||||
trap 'exit' ERR
|
||||
echo "Installing tools"
|
||||
apt-get update -qq
|
||||
apt-get install -qq -o=Dpkg::Use-Pty=0 \
|
||||
git gcc curl attr libgit2-dev zip \
|
||||
fish zsh bash bash-completion
|
||||
fi
|
||||
EOF
|
||||
|
||||
|
||||
# Guarantee that the timezone is UTC -- some of the tests
|
||||
# depend on this (for now).
|
||||
config.vm.provision :shell, privileged: true, inline:
|
||||
%[timedatectl set-timezone UTC]
|
||||
|
||||
|
||||
# Install Rust.
|
||||
# This is done as vagrant, not root, because it’s vagrant
|
||||
# who actually uses it. Sent to /dev/null because the progress
|
||||
|
@ -49,25 +50,46 @@ Vagrant.configure(2) do |config|
|
|||
if hash rustc &>/dev/null; then
|
||||
echo "Rust is already installed"
|
||||
else
|
||||
set -xe
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
trap 'exit' ERR
|
||||
echo "Installing Rust"
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --profile minimal --component rustc,rust-std,cargo,clippy -y > /dev/null
|
||||
source $HOME/.cargo/env
|
||||
echo "Installing cargo-hack"
|
||||
cargo install -q cargo-hack
|
||||
echo "Installing specsheet"
|
||||
cargo install -q --git https://github.com/ogham/specsheet
|
||||
fi
|
||||
EOF
|
||||
|
||||
|
||||
# Use a different ‘target’ directory on the VM than on the host.
|
||||
# By default it just uses the one in /vagrant/target, which can
|
||||
# cause problems if it has different permissions than the other
|
||||
# directories, or contains object files compiled for the host.
|
||||
# Privileged installation and setup scripts.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
|
||||
# Install Just, the command runner.
|
||||
if hash just &>/dev/null; then
|
||||
echo "just is already installed"
|
||||
else
|
||||
trap 'exit' ERR
|
||||
echo "Installing just"
|
||||
wget -q "https://github.com/casey/just/releases/download/v0.8.3/just-v0.8.3-x86_64-unknown-linux-musl.tar.gz"
|
||||
tar -xf "just-v0.8.3-x86_64-unknown-linux-musl.tar.gz"
|
||||
cp just /usr/local/bin
|
||||
fi
|
||||
|
||||
# Guarantee that the timezone is UTC — some of the tests
|
||||
# depend on this (for now).
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
|
||||
# Use a different ‘target’ directory on the VM than on the host.
|
||||
# By default it just uses the one in /vagrant/target, which can
|
||||
# cause problems if it has different permissions than the other
|
||||
# directories, or contains object files compiled for the host.
|
||||
echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/#{developer}/.cargo/bin"' > /etc/environment
|
||||
echo 'CARGO_TARGET_DIR="/home/#{developer}/target"' >> /etc/environment
|
||||
EOF
|
||||
|
||||
|
||||
# Create a variety of misc scripts.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
# Create a variety of misc scripts.
|
||||
|
||||
ln -sf /vagrant/devtools/dev-run-debug.sh /usr/bin/exa
|
||||
ln -sf /vagrant/devtools/dev-run-release.sh /usr/bin/rexa
|
||||
|
@ -75,7 +97,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
|
||||
|
@ -88,22 +110,12 @@ Vagrant.configure(2) do |config|
|
|||
echo -e "#!/bin/sh\ncat /etc/motd" > /usr/bin/halp
|
||||
|
||||
chmod +x /usr/bin/{exa,rexa,b,t,x,c,build-exa,test-exa,run-xtests,compile-exa,package-exa,halp}
|
||||
EOF
|
||||
|
||||
|
||||
# This fix is applied by changing the VM rather than changing the
|
||||
# Cargo.toml file so it works for everyone because it’s such a niche
|
||||
# build issue, it’s not worth specifying a non-crates.io dependency
|
||||
# and losing the ability to `cargo publish` the exa crate there!
|
||||
# It also isolates the hackiness to the one place I can test it
|
||||
# actually works.
|
||||
|
||||
|
||||
# Configure the welcoming text that gets shown.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
rm -f /etc/update-motd.d/*
|
||||
# Configure the welcoming text that gets shown:
|
||||
|
||||
# Capture the help text so it gets displayed first
|
||||
rm -f /etc/update-motd.d/*
|
||||
bash /vagrant/devtools/dev-help.sh > /etc/motd
|
||||
|
||||
# Tell bash to execute a bunch of stuff when a session starts
|
||||
|
@ -113,423 +125,29 @@ Vagrant.configure(2) do |config|
|
|||
# Disable last login date in sshd
|
||||
sed -i '/PrintLastLog yes/c\PrintLastLog no' /etc/ssh/sshd_config
|
||||
systemctl restart sshd
|
||||
EOF
|
||||
|
||||
|
||||
# Link the completion files so they’re “installed”.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
# Link the completion files so they’re “installed”:
|
||||
|
||||
# bash
|
||||
test -h /etc/bash_completion.d/exa \
|
||||
|| ln -s /vagrant/contrib/completions.bash /etc/bash_completion.d/exa
|
||||
|
||||
# zsh
|
||||
test -h /usr/share/zsh/vendor-completions/_exa \
|
||||
|| ln -s /vagrant/contrib/completions.zsh /usr/share/zsh/vendor-completions/_exa
|
||||
|
||||
# fish
|
||||
test -h /usr/share/fish/completions/exa.fish \
|
||||
|| ln -s /vagrant/contrib/completions.fish /usr/share/fish/completions/exa.fish
|
||||
EOF
|
||||
|
||||
|
||||
# We create two users that own the test files.
|
||||
# The first one just owns the ordinary ones, because we don’t want the
|
||||
# test outputs to depend on “vagrant” or “ubuntu” existing.
|
||||
user = "cassowary"
|
||||
config.vm.provision :shell, privileged: true, inline:
|
||||
%[id -u #{user} &>/dev/null || useradd #{user}]
|
||||
|
||||
|
||||
# The second one has a long name, to test that the file owner column
|
||||
# widens correctly. The benefit of Vagrant is that we don’t need to
|
||||
# set this up on the *actual* system!
|
||||
longuser = "antidisestablishmentarienism"
|
||||
config.vm.provision :shell, privileged: true, inline:
|
||||
%[id -u #{longuser} &>/dev/null || useradd #{longuser}]
|
||||
|
||||
|
||||
# Because the timestamps are formatted differently depending on whether
|
||||
# they’re in the current year or not (see `details.rs`), we have to make
|
||||
# sure that the files are created in the current year, so they get shown
|
||||
# in the format we expect.
|
||||
current_year = Date.today.year
|
||||
some_date = "#{current_year}01011234.56" # 1st January, 12:34:56
|
||||
|
||||
|
||||
# We also need an UID and a GID that are guaranteed to not exist, to
|
||||
# test what happen when they don’t.
|
||||
invalid_uid = 666
|
||||
invalid_gid = 616
|
||||
|
||||
|
||||
# Delete old testcases if they exist already, then create a
|
||||
# directory to house new ones.
|
||||
test_dir = "/testcases"
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
rm -rfv #{test_dir}
|
||||
mkdir #{test_dir}
|
||||
chmod 777 #{test_dir}
|
||||
EOF
|
||||
|
||||
|
||||
# Awkward file size testcases.
|
||||
# This needs sudo to set the files’ users at the very end.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/files"
|
||||
for i in {1..13}; do
|
||||
fallocate -l "$i" "#{test_dir}/files/$i"_bytes
|
||||
fallocate -l "$i"KiB "#{test_dir}/files/$i"_KiB
|
||||
fallocate -l "$i"MiB "#{test_dir}/files/$i"_MiB
|
||||
done
|
||||
|
||||
touch -t #{some_date} "#{test_dir}/files/"*
|
||||
chmod 644 "#{test_dir}/files/"*
|
||||
sudo chown #{user}:#{user} "#{test_dir}/files/"*
|
||||
EOF
|
||||
|
||||
|
||||
# File name extension testcases.
|
||||
# These aren’t tested in details view, but we set timestamps on them to
|
||||
# test that various sort options work.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/file-names-exts"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/Makefile"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/IMAGE.PNG"
|
||||
touch "#{test_dir}/file-names-exts/image.svg"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/VIDEO.AVI"
|
||||
touch "#{test_dir}/file-names-exts/video.wmv"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/music.mp3"
|
||||
touch "#{test_dir}/file-names-exts/MUSIC.OGG"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/lossless.flac"
|
||||
touch "#{test_dir}/file-names-exts/lossless.wav"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/crypto.asc"
|
||||
touch "#{test_dir}/file-names-exts/crypto.signature"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/document.pdf"
|
||||
touch "#{test_dir}/file-names-exts/DOCUMENT.XLSX"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/COMPRESSED.ZIP"
|
||||
touch "#{test_dir}/file-names-exts/compressed.tar.gz"
|
||||
touch "#{test_dir}/file-names-exts/compressed.tgz"
|
||||
touch "#{test_dir}/file-names-exts/compressed.tar.xz"
|
||||
touch "#{test_dir}/file-names-exts/compressed.txz"
|
||||
touch "#{test_dir}/file-names-exts/compressed.deb"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/backup~"
|
||||
touch "#{test_dir}/file-names-exts/#SAVEFILE#"
|
||||
touch "#{test_dir}/file-names-exts/file.tmp"
|
||||
|
||||
touch "#{test_dir}/file-names-exts/compiled.class"
|
||||
touch "#{test_dir}/file-names-exts/compiled.o"
|
||||
touch "#{test_dir}/file-names-exts/compiled.js"
|
||||
touch "#{test_dir}/file-names-exts/compiled.coffee"
|
||||
EOF
|
||||
|
||||
|
||||
# File name testcases.
|
||||
# bash really doesn’t want you to create a file with escaped characters
|
||||
# in its name, so we have to resort to the echo builtin and touch!
|
||||
#
|
||||
# The double backslashes are not strictly necessary; without them, Ruby
|
||||
# will interpolate them instead of bash, but because Vagrant prints out
|
||||
# each command it runs, your *own* terminal will go “ding” from the alarm!
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/file-names"
|
||||
|
||||
echo -ne "#{test_dir}/file-names/ascii: hello" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/emoji: [🆒]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/utf-8: pâté" | xargs -0 touch
|
||||
|
||||
echo -ne "#{test_dir}/file-names/bell: [\\a]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/backspace: [\\b]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/form-feed: [\\f]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/new-line: [\\n]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/return: [\\r]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/tab: [\\t]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/vertical-tab: [\\v]" | xargs -0 touch
|
||||
|
||||
echo -ne "#{test_dir}/file-names/escape: [\\033]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/ansi: [\\033[34mblue\\033[0m]" | xargs -0 touch
|
||||
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-1: [\\xFF]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-2: [\\xc3\\x28]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-3: [\\xe2\\x82\\x28]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/invalid-utf8-4: [\\xf0\\x28\\x8c\\x28]" | xargs -0 touch
|
||||
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]" | xargs -0 mkdir
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/subfile" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/another: [\\n]" | xargs -0 touch
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken" | xargs -0 touch
|
||||
|
||||
mkdir "#{test_dir}/file-names/links"
|
||||
ln -s "#{test_dir}/file-names/new-line-dir"*/* "#{test_dir}/file-names/links"
|
||||
|
||||
echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken" | xargs -0 rm
|
||||
EOF
|
||||
|
||||
|
||||
# Special file testcases.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/specials"
|
||||
|
||||
sudo mknod "#{test_dir}/specials/block-device" b 3 60
|
||||
sudo mknod "#{test_dir}/specials/char-device" c 14 40
|
||||
sudo mknod "#{test_dir}/specials/named-pipe" p
|
||||
|
||||
sudo touch -t #{some_date} "#{test_dir}/specials/"*
|
||||
EOF
|
||||
|
||||
|
||||
# Awkward symlink testcases.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/links"
|
||||
|
||||
ln -s / "#{test_dir}/links/root"
|
||||
ln -s /usr "#{test_dir}/links/usr"
|
||||
ln -s nowhere "#{test_dir}/links/broken"
|
||||
ln -s /proc/1/root "#{test_dir}/links/forbidden"
|
||||
|
||||
touch "#{test_dir}/links/some_file"
|
||||
ln -s "#{test_dir}/links/some_file" "#{test_dir}/links/some_file_absolute"
|
||||
(cd "#{test_dir}/links"; ln -s "some_file" "some_file_relative")
|
||||
(cd "#{test_dir}/links"; ln -s "." "current_dir")
|
||||
(cd "#{test_dir}/links"; ln -s ".." "parent_dir")
|
||||
(cd "#{test_dir}/links"; ln -s "itself" "itself")
|
||||
EOF
|
||||
|
||||
|
||||
# Awkward passwd testcases.
|
||||
# sudo is needed for these because we technically aren’t a member
|
||||
# of the groups (because they don’t exist), and chown and chgrp
|
||||
# are smart enough to disallow it!
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/passwd"
|
||||
|
||||
touch -t #{some_date} "#{test_dir}/passwd/unknown-uid"
|
||||
chmod 644 "#{test_dir}/passwd/unknown-uid"
|
||||
sudo chown #{invalid_uid}:#{user} "#{test_dir}/passwd/unknown-uid"
|
||||
|
||||
touch -t #{some_date} "#{test_dir}/passwd/unknown-gid"
|
||||
chmod 644 "#{test_dir}/passwd/unknown-gid"
|
||||
sudo chown #{user}:#{invalid_gid} "#{test_dir}/passwd/unknown-gid"
|
||||
EOF
|
||||
|
||||
|
||||
# Awkward permission testcases.
|
||||
# Differences in the way ‘chmod’ handles setting ‘setuid’ and ‘setgid’
|
||||
# when you don’t already own the file mean that we need to use ‘sudo’
|
||||
# to change permissions to those.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/permissions"
|
||||
|
||||
mkdir "#{test_dir}/permissions/forbidden-directory"
|
||||
chmod 000 "#{test_dir}/permissions/forbidden-directory"
|
||||
touch -t #{some_date} "#{test_dir}/permissions/forbidden-directory"
|
||||
sudo chown #{user}:#{user} "#{test_dir}/permissions/forbidden-directory"
|
||||
|
||||
for perms in 000 001 002 004 010 020 040 100 200 400 644 755 777 1000 1001 2000 2010 4000 4100 7666 7777; do
|
||||
touch "#{test_dir}/permissions/$perms"
|
||||
sudo chown #{user}:#{user} "#{test_dir}/permissions/$perms"
|
||||
sudo chmod $perms "#{test_dir}/permissions/$perms"
|
||||
sudo touch -t #{some_date} "#{test_dir}/permissions/$perms"
|
||||
done
|
||||
EOF
|
||||
|
||||
old = '200303030000.00'
|
||||
med = '200606152314.29' # the june gets used for fr_FR locale tests
|
||||
new = '200912221038.53' # and the december for ja_JP local tests
|
||||
|
||||
# Awkward date and time testcases.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/dates"
|
||||
|
||||
# there's no way to touch the created date of a file...
|
||||
# so we have to do this the old-fashioned way!
|
||||
# (and make sure these don't actually get listed)
|
||||
touch -t #{old} "#{test_dir}/dates/peach"; sleep 1
|
||||
touch -t #{med} "#{test_dir}/dates/plum"; sleep 1
|
||||
touch -t #{new} "#{test_dir}/dates/pear"
|
||||
|
||||
# modified dates
|
||||
touch -t #{old} -m "#{test_dir}/dates/pear"
|
||||
touch -t #{med} -m "#{test_dir}/dates/peach"
|
||||
touch -t #{new} -m "#{test_dir}/dates/plum"
|
||||
|
||||
# accessed dates
|
||||
touch -t #{old} -a "#{test_dir}/dates/plum"
|
||||
touch -t #{med} -a "#{test_dir}/dates/pear"
|
||||
touch -t #{new} -a "#{test_dir}/dates/peach"
|
||||
|
||||
sudo chown #{user}:#{user} -R "#{test_dir}/dates"
|
||||
EOF
|
||||
|
||||
|
||||
# Awkward extended attribute testcases.
|
||||
# We need to test combinations of various numbers of files *and*
|
||||
# extended attributes in directories. Turns out, the easiest way to
|
||||
# do this is to generate all combinations of files with “one-xattr”
|
||||
# or “two-xattrs” in their name and directories with “empty” or
|
||||
# “one-file” in their name, then just give the right number of
|
||||
# xattrs and children to those.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/attributes"
|
||||
|
||||
mkdir "#{test_dir}/attributes/files"
|
||||
touch "#{test_dir}/attributes/files/"{no-xattrs,one-xattr,two-xattrs}{,_forbidden}
|
||||
|
||||
mkdir "#{test_dir}/attributes/dirs"
|
||||
mkdir "#{test_dir}/attributes/dirs/"{no-xattrs,one-xattr,two-xattrs}_{empty,one-file,two-files}{,_forbidden}
|
||||
|
||||
setfattr -n user.greeting -v hello "#{test_dir}/attributes"/**/*{one-xattr,two-xattrs}*
|
||||
setfattr -n user.another_greeting -v hi "#{test_dir}/attributes"/**/*two-xattrs*
|
||||
|
||||
for dir in "#{test_dir}/attributes/dirs/"*one-file*; do
|
||||
touch $dir/file-in-question
|
||||
done
|
||||
|
||||
for dir in "#{test_dir}/attributes/dirs/"*two-files*; do
|
||||
touch $dir/this-file
|
||||
touch $dir/that-file
|
||||
done
|
||||
|
||||
find "#{test_dir}/attributes" -exec touch {} -t #{some_date} \\;
|
||||
|
||||
# I want to use the following to test,
|
||||
# but it only works on macos:
|
||||
#chmod +a "#{user} deny readextattr" "#{test_dir}/attributes"/**/*_forbidden
|
||||
|
||||
sudo chmod 000 "#{test_dir}/attributes"/**/*_forbidden
|
||||
sudo chown #{user}:#{user} -R "#{test_dir}/attributes"
|
||||
EOF
|
||||
|
||||
|
||||
# A sample Git repository
|
||||
# This uses cd because it's easier than telling Git where to go each time
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir "#{test_dir}/git"
|
||||
cd "#{test_dir}/git"
|
||||
git init
|
||||
|
||||
mkdir edits additions moves
|
||||
|
||||
echo "original content" | tee edits/{staged,unstaged,both}
|
||||
echo "this file gets moved" > moves/hither
|
||||
|
||||
git add edits moves
|
||||
git config --global user.email "exa@exa.exa"
|
||||
git config --global user.name "Exa Exa"
|
||||
git commit -m "Automated test commit"
|
||||
|
||||
echo "modifications!" | tee edits/{staged,both}
|
||||
touch additions/{staged,edited}
|
||||
mv moves/{hither,thither}
|
||||
|
||||
git add edits moves additions
|
||||
echo "more modifications!" | tee edits/unstaged edits/both additions/edited
|
||||
touch additions/unstaged
|
||||
|
||||
find "#{test_dir}/git" -exec touch {} -t #{some_date} \\;
|
||||
sudo chown #{user}:#{user} -R "#{test_dir}/git"
|
||||
EOF
|
||||
|
||||
|
||||
# A second Git repository
|
||||
# for testing two at once
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
mkdir -p "#{test_dir}/git2/deeply/nested/directory"
|
||||
cd "#{test_dir}/git2"
|
||||
git init
|
||||
|
||||
touch "deeply/nested/directory/upd8d"
|
||||
git add "deeply/nested/directory/upd8d"
|
||||
git commit -m "Automated test commit"
|
||||
|
||||
echo "Now with contents" > "deeply/nested/directory/upd8d"
|
||||
touch "deeply/nested/directory/l8st"
|
||||
|
||||
echo -e "target\n*.mp3" > ".gitignore"
|
||||
mkdir "ignoreds"
|
||||
touch "ignoreds/music.mp3"
|
||||
touch "ignoreds/music.m4a"
|
||||
mkdir "ignoreds/nested"
|
||||
touch "ignoreds/nested/70s grove.mp3"
|
||||
touch "ignoreds/nested/funky chicken.m4a"
|
||||
|
||||
mkdir "target"
|
||||
touch "target/another ignored file"
|
||||
|
||||
mkdir "deeply/nested/repository"
|
||||
cd "deeply/nested/repository"
|
||||
git init
|
||||
touch subfile
|
||||
|
||||
find "#{test_dir}/git2" -exec touch {} -t #{some_date} \\;
|
||||
sudo chown #{user}:#{user} -R "#{test_dir}/git2"
|
||||
EOF
|
||||
|
||||
# Hidden and dot file testcases.
|
||||
# We need to set the permissions of `.` and `..` because they actually
|
||||
# get displayed in the output here, so this has to come last.
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
shopt -u dotglob
|
||||
GLOBIGNORE=".:.."
|
||||
|
||||
mkdir "#{test_dir}/hiddens"
|
||||
touch "#{test_dir}/hiddens/visible"
|
||||
touch "#{test_dir}/hiddens/.hidden"
|
||||
touch "#{test_dir}/hiddens/..extra-hidden"
|
||||
|
||||
# ./hiddens/
|
||||
touch -t #{some_date} "#{test_dir}/hiddens/"*
|
||||
chmod 644 "#{test_dir}/hiddens/"*
|
||||
sudo chown #{user}:#{user} "#{test_dir}/hiddens/"*
|
||||
|
||||
# .
|
||||
touch -t #{some_date} "#{test_dir}/hiddens"
|
||||
chmod 755 "#{test_dir}/hiddens"
|
||||
sudo chown #{user}:#{user} "#{test_dir}/hiddens"
|
||||
|
||||
# ..
|
||||
sudo touch -t #{some_date} "#{test_dir}"
|
||||
sudo chmod 755 "#{test_dir}"
|
||||
sudo chown #{user}:#{user} "#{test_dir}"
|
||||
EOF
|
||||
|
||||
|
||||
# Set up some locales
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
|
||||
# uncomment these from the config file
|
||||
sudo sed -i '/fr_FR.UTF-8/s/^# //g' /etc/locale.gen
|
||||
sudo sed -i '/ja_JP.UTF-8/s/^# //g' /etc/locale.gen
|
||||
sudo locale-gen
|
||||
EOF
|
||||
|
||||
|
||||
# Install kcov for test coverage
|
||||
# This doesn’t run coverage over the xtests so it’s less useful for now
|
||||
if ENV.key?('INSTALL_KCOV')
|
||||
config.vm.provision :shell, privileged: false, inline: <<-EOF
|
||||
set -xe
|
||||
trap 'exit' ERR
|
||||
|
||||
test -e ~/.cargo/bin/cargo-kcov \
|
||||
|| cargo install cargo-kcov
|
||||
|
@ -541,48 +159,8 @@ Vagrant.configure(2) do |config|
|
|||
cargo kcov --print-install-kcov-sh | sudo sh
|
||||
EOF
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Remember that problem that exa had where the binary wasn’t actually
|
||||
# self-contained? Or the problem where the Linux binary was actually the
|
||||
# macOS binary in disguise?
|
||||
#
|
||||
# This is a “fresh” VM that intentionally downloads no dependencies and
|
||||
# installs nothing so that we can check that exa still runs!
|
||||
config.vm.define(:fresh) do |config|
|
||||
config.vm.box = UBUNTU
|
||||
config.vm.hostname = 'fresh'
|
||||
|
||||
config.vm.provider :virtualbox do |v|
|
||||
v.name = 'exa-fresh'
|
||||
v.memory = 384
|
||||
v.cpus = 1
|
||||
end
|
||||
|
||||
# Well, we do need *one* dependency...
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
apt-get install -qq -o=Dpkg::Use-Pty=0 -y unzip
|
||||
EOF
|
||||
|
||||
# This thing also has its own welcoming text.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
rm -f /etc/update-motd.d/*
|
||||
|
||||
# Capture the help text so it gets displayed first
|
||||
bash /vagrant/devtools/dev-help-testvm.sh > /etc/motd
|
||||
|
||||
# Disable last login date in sshd
|
||||
sed -i '/PrintLastLog yes/c\PrintLastLog no' /etc/ssh/sshd_config
|
||||
systemctl restart sshd
|
||||
EOF
|
||||
|
||||
# Make the checker script a command.
|
||||
config.vm.provision :shell, privileged: true, inline: <<-EOF
|
||||
set -xe
|
||||
echo -e "#!/bin/sh\nbash /vagrant/devtools/dev-download-and-check-release.sh \"\\$*\"" > /usr/bin/check-release
|
||||
chmod +x /usr/bin/check-release
|
||||
EOF
|
||||
config.vm.provision :shell, privileged: true, path: 'devtools/dev-set-up-environment.sh'
|
||||
config.vm.provision :shell, privileged: false, path: 'devtools/dev-create-test-filesystem.sh'
|
||||
end
|
||||
end
|
||||
|
|
112
build.rs
112
build.rs
|
@ -10,10 +10,51 @@
|
|||
/// - https://stackoverflow.com/q/43753491/3484614
|
||||
/// - https://crates.io/crates/vergen
|
||||
|
||||
extern crate datetime;
|
||||
use std::io::Result as IOResult;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use datetime::{LocalDateTime, ISO};
|
||||
|
||||
|
||||
/// The build script entry point.
|
||||
fn main() -> io::Result<()> {
|
||||
#![allow(clippy::write_with_newline)]
|
||||
|
||||
let tagline = "exa - list files on the command-line";
|
||||
let url = "https://the.exa.website/";
|
||||
|
||||
let ver =
|
||||
if is_debug_build() {
|
||||
format!("{}\nv{} \\1;31m(pre-release debug build!)\\0m\n\\1;4;34m{}\\0m", tagline, version_string(), url)
|
||||
}
|
||||
else if is_development_version() {
|
||||
format!("{}\nv{} [{}] built on {} \\1;31m(pre-release!)\\0m\n\\1;4;34m{}\\0m", tagline, version_string(), git_hash(), build_date(), url)
|
||||
}
|
||||
else {
|
||||
format!("{}\nv{}\n\\1;4;34m{}\\0m", tagline, version_string(), url)
|
||||
};
|
||||
|
||||
// We need to create these files in the Cargo output directory.
|
||||
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let path = &out.join("version_string.txt");
|
||||
|
||||
// Bland version text
|
||||
let mut f = File::create(path).unwrap_or_else(|_| { panic!("{}", path.to_string_lossy().to_string()) });
|
||||
writeln!(f, "{}", strip_codes(&ver))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes escape codes from a string.
|
||||
fn strip_codes(input: &str) -> String {
|
||||
input.replace("\\0m", "")
|
||||
.replace("\\1;31m", "")
|
||||
.replace("\\1;4;34m", "")
|
||||
}
|
||||
|
||||
/// Retrieve the project’s current Git hash, as a string.
|
||||
fn git_hash() -> String {
|
||||
use std::process::Command;
|
||||
|
||||
|
@ -24,38 +65,59 @@ fn git_hash() -> String {
|
|||
.stdout).trim().to_string()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
write_statics().unwrap();
|
||||
}
|
||||
|
||||
/// Whether we should show pre-release info in the version string.
|
||||
///
|
||||
/// Both weekly releases and actual releases are --release releases,
|
||||
/// but actual releases will have a proper version number.
|
||||
fn is_development_version() -> bool {
|
||||
// Both weekly releases and actual releases are --release releases,
|
||||
// but actual releases will have a proper version number
|
||||
cargo_version().ends_with("-pre") || env::var("PROFILE").unwrap() == "debug"
|
||||
}
|
||||
|
||||
/// Whether we are building in debug mode.
|
||||
fn is_debug_build() -> bool {
|
||||
env::var("PROFILE").unwrap() == "debug"
|
||||
}
|
||||
|
||||
/// Retrieves the [package] version in Cargo.toml as a string.
|
||||
fn cargo_version() -> String {
|
||||
env::var("CARGO_PKG_VERSION").unwrap()
|
||||
}
|
||||
|
||||
fn build_date() -> String {
|
||||
use datetime::{LocalDateTime, ISO};
|
||||
/// Returns the version and build parameters string.
|
||||
fn version_string() -> String {
|
||||
let mut ver = cargo_version();
|
||||
|
||||
let feats = nonstandard_features_string();
|
||||
if ! feats.is_empty() {
|
||||
ver.push_str(&format!(" [{}]", &feats));
|
||||
}
|
||||
|
||||
ver
|
||||
}
|
||||
|
||||
/// Finds whether a feature is enabled by examining the Cargo variable.
|
||||
fn feature_enabled(name: &str) -> bool {
|
||||
env::var(&format!("CARGO_FEATURE_{}", name))
|
||||
.map(|e| ! e.is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// A comma-separated list of non-standard feature choices.
|
||||
fn nonstandard_features_string() -> String {
|
||||
let mut s = Vec::new();
|
||||
|
||||
if feature_enabled("GIT") {
|
||||
s.push("+git");
|
||||
}
|
||||
else {
|
||||
s.push("-git");
|
||||
}
|
||||
|
||||
s.join(", ")
|
||||
}
|
||||
|
||||
/// Formats the current date as an ISO 8601 string.
|
||||
fn build_date() -> String {
|
||||
let now = LocalDateTime::now();
|
||||
format!("{}", now.date().iso())
|
||||
}
|
||||
|
||||
fn write_statics() -> IOResult<()> {
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
let ver = match is_development_version() {
|
||||
true => format!("exa v{} ({} built on {})", cargo_version(), git_hash(), build_date()),
|
||||
false => format!("exa v{}", cargo_version()),
|
||||
};
|
||||
|
||||
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let mut f = File::create(&out.join("version_string.txt"))?;
|
||||
write!(f, "{:?}", ver)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,11 @@ _exa()
|
|||
return
|
||||
;;
|
||||
|
||||
--colour)
|
||||
COMPREPLY=( $( compgen -W 'always auto never' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
|
||||
-L|--level)
|
||||
COMPREPLY=( $( compgen -W '{0..9}' -- "$cur" ) )
|
||||
return
|
||||
|
@ -19,19 +24,28 @@ _exa()
|
|||
;;
|
||||
|
||||
-t|--time)
|
||||
COMPREPLY=( $( compgen -W 'modified changed accessed created --' -- $cur ) )
|
||||
COMPREPLY=( $( compgen -W 'modified changed accessed created --' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
|
||||
--time-style)
|
||||
COMPREPLY=( $( compgen -W 'default iso long-iso full-iso --' -- $cur ) )
|
||||
COMPREPLY=( $( compgen -W 'default iso long-iso full-iso --' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
# _parse_help doesn’t pick up short options when they are on the same line than long options
|
||||
--*)
|
||||
# colo[u]r isn’t parsed correctly so we filter these options out and add them by hand
|
||||
parse_help=$( exa --help | grep -oE ' (\-\-[[:alnum:]@-]+)' | tr -d ' ' | grep -v '\-\-colo' )
|
||||
completions=$( echo '--color --colour --color-scale --colour-scale' $parse_help )
|
||||
COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) )
|
||||
;;
|
||||
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) )
|
||||
completions=$( exa --help | grep -oE ' (\-[[:alnum:]@])' | tr -d ' ' )
|
||||
COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) )
|
||||
;;
|
||||
|
||||
*)
|
|
@ -10,19 +10,25 @@ complete -c exa -s 'x' -l 'across' -d "Sort the grid across, rather than d
|
|||
complete -c exa -s 'R' -l 'recurse' -d "Recurse into directories"
|
||||
complete -c exa -s 'T' -l 'tree' -d "Recurse into directories as a tree"
|
||||
complete -c exa -s 'F' -l 'classify' -d "Display type indicator by file names"
|
||||
complete -c exa -l 'color' -d "When to use terminal colours"
|
||||
complete -c exa -l 'colour' -d "When to use terminal colours"
|
||||
complete -c exa -l 'color-scale' -d "Highlight levels of file sizes distinctly"
|
||||
complete -c exa -l 'colour-scale' -d "Highlight levels of file sizes distinctly"
|
||||
complete -c exa -l 'color' \
|
||||
-l 'colour' -d "When to use terminal colours" -x -a "
|
||||
always\t'Always use colour'
|
||||
auto\t'Use colour if standard output is a terminal'
|
||||
never\t'Never use colour'
|
||||
"
|
||||
complete -c exa -l 'color-scale' \
|
||||
-l 'colour-scale' -d "Highlight levels of file sizes distinctly"
|
||||
complete -c exa -l 'icons' -d "Display icons"
|
||||
complete -c exa -l 'no-icons' -d "Don't display icons"
|
||||
|
||||
# Filtering and sorting options
|
||||
complete -c exa -l 'group-directories-first' -d "Sort directories before other files"
|
||||
complete -c exa -l 'git-ignore' -d "Ignore files mentioned in '.gitignore'"
|
||||
complete -c exa -s 'a' -l 'all' -d "Show and 'dot' files"
|
||||
complete -c exa -s 'a' -l 'all' -d "Show hidden and 'dot' files"
|
||||
complete -c exa -s 'd' -l 'list-dirs' -d "List directories like regular files"
|
||||
complete -c exa -s 'L' -l 'level' -d "Limit the depth of recursion" -a "1 2 3 4 5 6 7 8 9"
|
||||
complete -c exa -s 'L' -l 'level' -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9"
|
||||
complete -c exa -s 'r' -l 'reverse' -d "Reverse the sort order"
|
||||
complete -c exa -s 's' -l 'sort' -x -d "Which field to sort by" -a "
|
||||
complete -c exa -s 's' -l 'sort' -d "Which field to sort by" -x -a "
|
||||
accessed\t'Sort by file accessed time'
|
||||
age\t'Sort by file modified time (newest first)'
|
||||
changed\t'Sort by changed time'
|
||||
|
@ -47,32 +53,39 @@ complete -c exa -s 's' -l 'sort' -x -d "Which field to sort by" -a "
|
|||
"
|
||||
|
||||
complete -c exa -s 'I' -l 'ignore-glob' -d "Ignore files that match these glob patterns" -r
|
||||
complete -c exa -s 'D' -l 'only-dirs' -d "List only directories"
|
||||
|
||||
# Long view options
|
||||
complete -c exa -s 'b' -l 'binary' -d "List file sizes with binary prefixes"
|
||||
complete -c exa -s 'B' -l 'bytes' -d "List file sizes in bytes, without any prefixes"
|
||||
complete -c exa -s 'g' -l 'group' -d "List each file's group"
|
||||
complete -c exa -s 'h' -l 'header' -d "Add a header row to each column"
|
||||
complete -c exa -s 'h' -l 'links' -d "List each file's number of hard links"
|
||||
complete -c exa -s 'g' -l 'group' -d "List each file's inode number"
|
||||
complete -c exa -s 'H' -l 'links' -d "List each file's number of hard links"
|
||||
complete -c exa -s 'i' -l 'inode' -d "List each file's inode number"
|
||||
complete -c exa -s 'S' -l 'blocks' -d "List each file's number of filesystem blocks"
|
||||
complete -c exa -s 't' -l 'time' -x -d "Which timestamp field to list" -a "
|
||||
complete -c exa -s 't' -l 'time' -d "Which timestamp field to list" -x -a "
|
||||
modified\t'Display modified time'
|
||||
changed\t'Display changed time'
|
||||
accessed\t'Display accessed time'
|
||||
created\t'Display created time'
|
||||
"
|
||||
complete -c exa -s 'm' -l 'modified' -d "Use the modified timestamp field"
|
||||
complete -c exa -s 'n' -l 'numeric' -d "List numeric user and group IDs."
|
||||
complete -c exa -l 'changed' -d "Use the changed timestamp field"
|
||||
complete -c exa -s 'u' -l 'accessed' -d "Use the accessed timestamp field"
|
||||
complete -c exa -s 'U' -l 'created' -d "Use the created timestamp field"
|
||||
complete -c exa -l 'time-style' -x -d "How to format timestamps" -a "
|
||||
complete -c exa -l 'time-style' -d "How to format timestamps" -x -a "
|
||||
default\t'Use the default time style'
|
||||
iso\t'Display brief ISO timestamps'
|
||||
long-iso\t'Display longer ISO timestaps, up to the minute'
|
||||
full-iso\t'Display full ISO timestamps, up to the nanosecond'
|
||||
"
|
||||
complete -c exa -l 'no-permissions' -d "Suppress the permissions field"
|
||||
complete -c exa -l 'octal-permissions' -d "List each file's permission in octal format"
|
||||
complete -c exa -l 'no-filesize' -d "Suppress the filesize field"
|
||||
complete -c exa -l 'no-user' -d "Suppress the user field"
|
||||
complete -c exa -l 'no-time' -d "Suppress the time field"
|
||||
|
||||
# Optional extras
|
||||
complete -c exa -s 'g' -l 'git' -d "List each file's Git status, if tracked"
|
||||
complete -c exa -l 'git' -d "List each file's Git status, if tracked"
|
||||
complete -c exa -s '@' -l 'extended' -d "List each file's extended attributes and sizes"
|
|
@ -1,7 +1,7 @@
|
|||
#compdef exa
|
||||
|
||||
# Save this file as _exa in /usr/local/share/zsh/site-functions or in any
|
||||
# other folder in $fpath. E. g. save it in a folder called ~/.zfunc and add a
|
||||
# other folder in $fpath. E.g. save it in a folder called ~/.zfunc and add a
|
||||
# line containing `fpath=(~/.zfunc $fpath)` somewhere before `compinit` in your
|
||||
# ~/.zshrc.
|
||||
|
||||
|
@ -19,26 +19,36 @@ __exa() {
|
|||
{-R,--recurse}"[Recurse into directories]" \
|
||||
{-T,--tree}"[Recurse into directories as a tree]" \
|
||||
{-F,--classify}"[Display type indicator by file names]" \
|
||||
--colo{,u}r"[When to use terminal colours]" \
|
||||
--colo{,u}r="[When to use terminal colours]:(when):(always auto never)" \
|
||||
--colo{,u}r-scale"[Highlight levels of file sizes distinctly]" \
|
||||
--icons"[Display icons]" \
|
||||
--no-icons"[Hide icons]" \
|
||||
--group-directories-first"[Sort directories before other files]" \
|
||||
--git-ignore"[Ignore files mentioned in '.gitignore']" \
|
||||
{-a,--all}"[Show hidden and 'dot' files]" \
|
||||
{-d,--list-dirs}"[List directories like regular files]" \
|
||||
{-D,--only-dirs}"[List only directories]" \
|
||||
{-L,--level}"+[Limit the depth of recursion]" \
|
||||
{-r,--reverse}"[Reverse the sort order]" \
|
||||
{-s,--sort}="[Which field to sort by]:(sort field):(accessed age changed created date extension Extension filename Filename inode modified oldest name Name newest none size time type)" \
|
||||
{-I,--ignore-glob}"[Ignore files that match these glob patterns]" \
|
||||
{-b,--binary}"[List file sizes with binary prefixes]" \
|
||||
{-B,--bytes}"[List file sizes in bytes, without any prefixes]" \
|
||||
--changed"[Use the changed timestamp field]" \
|
||||
{-g,--group}"[List each file's group]" \
|
||||
{-h,--header}"[Add a header row to each column]" \
|
||||
{-H,--links}"[List each file's number of hard links]" \
|
||||
{-i,--inode}"[List each file's inode number]" \
|
||||
{-m,--modified}"[Use the modified timestamp field]" \
|
||||
{-n,--numeric}"[List numeric user and group IDs.]" \
|
||||
{-S,--blocks}"[List each file's number of filesystem blocks]" \
|
||||
{-t,--time}="[Which time field to show]:(time field):(accessed changed created modified)" \
|
||||
--time-style="[How to format timestamps]:(time style):(default iso long-iso full-iso)" \
|
||||
--no-permissions"[Suppress the permissions field]" \
|
||||
--octal-permissions"[List each file's permission in octal format]" \
|
||||
--no-filesize"[Suppress the filesize field]" \
|
||||
--no-user"[Suppress the user field]" \
|
||||
--no-time"[Suppress the time field]" \
|
||||
{-u,--accessed}"[Use the accessed timestamp field]" \
|
||||
{-U,--created}"[Use the created timestamp field]" \
|
||||
--git"[List each file's Git status, if tracked]" \
|
|
@ -1,451 +0,0 @@
|
|||
.hy
|
||||
.TH "exa" "1" "2019\-07\-15" "exa 0.9.0" ""
|
||||
.SH NAME
|
||||
.PP
|
||||
exa \- a modern replacement for ls
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
exa [\f[I]options\f[]] [\f[I]files\f[]]...
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
\f[C]exa\f[] is a modern replacement for \f[C]ls\f[].
|
||||
It uses colours for information by default, helping you distinguish
|
||||
between many types of files, such as whether you are the owner, or in
|
||||
the owning group.
|
||||
It also has extra features not present in the original \f[C]ls\f[], such
|
||||
as viewing the Git status for a directory, or recursing into directories
|
||||
with a tree view.
|
||||
.SH DISPLAY OPTIONS
|
||||
.TP
|
||||
.B \-1, \-\-oneline
|
||||
display one entry per line
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-G, \-\-grid
|
||||
display entries as a grid (default)
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-l, \-\-long
|
||||
display extended file metadata as a table
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-x, \-\-across
|
||||
sort the grid across, rather than downwards
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-R, \-\-recurse
|
||||
recurse into directories
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-T, \-\-tree
|
||||
recurse into directories as a tree
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-F, \-\-classify
|
||||
display type indicator by file names
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-color, \-\-colour=\f[I]WHEN\f[]
|
||||
when to use terminal colours (always, automatic, never)
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-color-scale, \-\-colour-scale
|
||||
highlight levels of file sizes distinctly
|
||||
.RS
|
||||
.RE
|
||||
.SH FILTERING AND SORTING OPTIONS
|
||||
.TP
|
||||
.B \-a, \-\-all
|
||||
show hidden and \[aq]dot\[aq] files.
|
||||
Use this twice to also show the \f[C].\f[] and \f[C]..\f[] directories.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-d, \-\-list\-dirs
|
||||
list directories like regular files
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-L, \-\-level=\f[I]DEPTH\f[]
|
||||
limit the depth of recursion
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-r, \-\-reverse
|
||||
reverse the sort order
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-s, \-\-sort=\f[I]SORT_FIELD\f[]
|
||||
which field to sort by.
|
||||
Valid fields are name, Name, extension, Extension, size, modified, changed, accessed, created, inode, type, and none.
|
||||
The modified field has the aliases date, time, and newest, and its reverse order has the aliases age and oldest.
|
||||
Fields starting with a capital letter will sort uppercase before lowercase: 'A' then 'B' then 'a' then 'b'.
|
||||
Fields starting with a lowercase letter will mix them: 'A' then 'a' then 'B' then 'b'.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-I, \-\-ignore\-glob=\f[I]GLOBS\f[]
|
||||
Glob patterns, pipe-separated, of files to ignore
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-git\-ignore
|
||||
ignore files mentioned in '.gitignore'
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-group\-directories\-first
|
||||
list directories before other files
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-D, \-\-only\-dirs
|
||||
list only directories
|
||||
.RS
|
||||
.RE
|
||||
.SH LONG VIEW OPTIONS
|
||||
.PP
|
||||
These options are available when running with \f[C]\-\-long\f[]
|
||||
(\f[C]\-l\f[]):
|
||||
.TP
|
||||
.B \-b, \-\-binary
|
||||
list file sizes with binary prefixes
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-B, \-\-bytes
|
||||
list file sizes in bytes, without any prefixes
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-g, \-\-group
|
||||
list each file\[aq]s group
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-h, \-\-header
|
||||
add a header row to each column
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-H, \-\-links
|
||||
list each file\[aq]s number of hard links
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-i, \-\-inode
|
||||
list each file\[aq]s inode number
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-m, \-\-modified
|
||||
use the modified timestamp field
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-S, \-\-blocks
|
||||
list each file\[aq]s number of file system blocks
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-t, \-\-time=\f[I]WORD\f[]
|
||||
which timestamp field to list (modified, changed, accessed, created)
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-time\-style=\f[I]STYLE\f[]
|
||||
how to format timestamps (default, iso, long-iso, full-iso)
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-u, \-\-accessed
|
||||
use the accessed timestamp field
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-U, \-\-created
|
||||
use the created timestamp field
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\@, \-\-extended
|
||||
list each file\[aq]s extended attributes and sizes
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-git
|
||||
list each file\[aq]s Git status, if tracked
|
||||
.RS
|
||||
.RE
|
||||
.SH EXAMPLES
|
||||
.PP
|
||||
To display a list of files, with the largest at the top:
|
||||
.IP
|
||||
.nf
|
||||
\f[C]
|
||||
exa\ \-\-reverse\ \-\-sort=size
|
||||
\f[]
|
||||
.fi
|
||||
.PP
|
||||
To display a tree of files, three levels deep:
|
||||
.IP
|
||||
.nf
|
||||
\f[C]
|
||||
exa\ \-\-long\ \-\-tree\ \-\-level=3
|
||||
\f[]
|
||||
.fi
|
||||
.SH ENVIRONMENT VARIABLES
|
||||
.PP
|
||||
exa responds to the following environment variables:
|
||||
.SS \f[C]COLUMNS\f[]
|
||||
.PP
|
||||
Overrides the width of the terminal, in characters.
|
||||
For example, \f[C]COLUMNS=80\ exa\f[] will show a grid view with a
|
||||
maximum width of 80 characters.
|
||||
.PP
|
||||
This option won\[aq]t do anything when exa\[aq]s output doesn\[aq]t
|
||||
wrap, such as when using the \f[C]\-\-long\f[] view.
|
||||
.SS \f[C]EXA_STRICT\f[]
|
||||
.PP
|
||||
Enables \f[I]strict mode\f[], which will make exa error when two
|
||||
command\-line options are incompatible.
|
||||
Usually, options can override each other going right\-to\-left on the
|
||||
command line, so that exa can be given aliases: creating an alias
|
||||
\f[C]exa=exa\ \-\-sort=ext\f[] then running \f[C]exa\ \-\-sort=size\f[]
|
||||
with that alias will run \f[C]exa\ \-\-sort=ext\ \-\-sort=size\f[], and
|
||||
the sorting specified by the user will override the sorting specified by
|
||||
the alias.
|
||||
In strict mode, the two options will not co\-operate, and exa will
|
||||
error.
|
||||
.PP
|
||||
This option is intended for use with automated scripts and other
|
||||
situations where you want to be \f[I]certain\f[] you\[aq]re typing in
|
||||
the right command.
|
||||
.SS \f[C]EXA_GRID_ROWS\f[]
|
||||
.PP
|
||||
Limits the grid\-details view (\f[C]exa\ \-\-grid\ \-\-long\f[]) so
|
||||
it\[aq]s only activated when at least the given number of rows of output
|
||||
would be generated.
|
||||
With widescreen displays, it\[aq]s possible for the grid to look very
|
||||
wide and sparse, on just one or two lines with none of the columns
|
||||
lining up.
|
||||
By specifying a minimum number of rows, you can only use the view if
|
||||
it\[aq]s going to be worth using.
|
||||
.SS \f[C]LS_COLORS\f[] and \f[C]EXA_COLORS\f[]
|
||||
.PP
|
||||
The \f[C]EXA_COLORS\f[] variable is the traditional way of customising
|
||||
the colours used by \f[C]ls\f[].
|
||||
.PP
|
||||
You can use the \f[C]dircolors\f[] program to generate a script that
|
||||
sets the variable from an input file, or if you don\[aq]t mind editing
|
||||
long strings of text, you can just type it out directly.
|
||||
These variables have the following structure:
|
||||
.IP \[bu] 2
|
||||
A list of key\-value pairs separated by \f[C]=\f[], such as
|
||||
\f[C]*.txt=32\f[].
|
||||
.IP \[bu] 2
|
||||
Multiple ANSI formatting codes are separated by \f[C];\f[], such as
|
||||
\f[C]*.txt=32;1;4\f[].
|
||||
.IP \[bu] 2
|
||||
Finally, multiple pairs are separated by \f[C]:\f[], such as
|
||||
\f[C]*.txt=32:*.mp3=1;35\f[].
|
||||
.PP
|
||||
The key half of the pair can either be a two\-letter code or a file
|
||||
glob, and anything that\[aq]s not a valid code will be treated as a
|
||||
glob, including keys that happen to be two letters long.
|
||||
.PP
|
||||
\f[C]LS_COLORS\f[] can use these ten codes:
|
||||
.IP \[bu] 2
|
||||
\f[B]di\f[], directories
|
||||
.IP \[bu] 2
|
||||
\f[B]ex\f[], executable files
|
||||
.IP \[bu] 2
|
||||
\f[B]fi\f[], regular files
|
||||
.IP \[bu] 2
|
||||
\f[B]pi\f[], named pipes
|
||||
.IP \[bu] 2
|
||||
\f[B]so\f[], sockets
|
||||
.IP \[bu] 2
|
||||
\f[B]bd\f[], block devices
|
||||
.IP \[bu] 2
|
||||
\f[B]cd\f[], character devices
|
||||
.IP \[bu] 2
|
||||
\f[B]ln\f[], symlinks
|
||||
.IP \[bu] 2
|
||||
\f[B]or\f[], symlinks with no target
|
||||
.PP
|
||||
\f[C]EXA_COLORS\f[] can use many more:
|
||||
.IP \[bu] 2
|
||||
\f[B]ur\f[], the user\-read permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]uw\f[], the user\-write permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]ux\f[], the user\-execute permission bit for regular files
|
||||
.IP \[bu] 2
|
||||
\f[B]ue\f[], the user\-execute for other file kinds
|
||||
.IP \[bu] 2
|
||||
\f[B]gr\f[], the group\-read permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]gw\f[], the group\-write permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]gx\f[], the group\-execute permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]tr\f[], the others\-read permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]tw\f[], the others\-write permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]tx\f[], the others\-execute permission bit
|
||||
.IP \[bu] 2
|
||||
\f[B]su\f[], setuid, setgid, and sticky permission bits for files
|
||||
.IP \[bu] 2
|
||||
\f[B]sf\f[], setuid, setgid, and sticky for other file kinds
|
||||
.IP \[bu] 2
|
||||
\f[B]xa\f[], the extended attribute indicator
|
||||
.IP \[bu] 2
|
||||
\f[B]sn\f[], the numbers of a file\[aq]s size
|
||||
.IP \[bu] 2
|
||||
\f[B]sb\f[], the units of a file\[aq]s size
|
||||
.IP \[bu] 2
|
||||
\f[B]df\f[], a device\[aq]s major ID
|
||||
.IP \[bu] 2
|
||||
\f[B]ds\f[], a device\[aq]s minor ID
|
||||
.IP \[bu] 2
|
||||
\f[B]uu\f[], a user that\[aq]s you
|
||||
.IP \[bu] 2
|
||||
\f[B]un\f[], a user that\[aq]s someone else
|
||||
.IP \[bu] 2
|
||||
\f[B]gu\f[], a group that you belong to
|
||||
.IP \[bu] 2
|
||||
\f[B]gn\f[], a group you aren\[aq]t a member of
|
||||
.IP \[bu] 2
|
||||
\f[B]lc\f[], a number of hard links
|
||||
.IP \[bu] 2
|
||||
\f[B]lm\f[], a number of hard links for a regular file with at least two
|
||||
.IP \[bu] 2
|
||||
\f[B]ga\f[], a new flag in Git
|
||||
.IP \[bu] 2
|
||||
\f[B]gm\f[], a modified flag in Git
|
||||
.IP \[bu] 2
|
||||
\f[B]gd\f[], a deleted flag in Git
|
||||
.IP \[bu] 2
|
||||
\f[B]gv\f[], a renamed flag in Git
|
||||
.IP \[bu] 2
|
||||
\f[B]gt\f[], a modified metadata flag in Git
|
||||
.IP \[bu] 2
|
||||
\f[B]xx\f[], "punctuation", including many background UI elements
|
||||
.IP \[bu] 2
|
||||
\f[B]da\f[], a file\[aq]s date
|
||||
.IP \[bu] 2
|
||||
\f[B]in\f[], a file\[aq]s inode number
|
||||
.IP \[bu] 2
|
||||
\f[B]bl\f[], a file\[aq]s number of blocks
|
||||
.IP \[bu] 2
|
||||
\f[B]hd\f[], the header row of a table
|
||||
.IP \[bu] 2
|
||||
\f[B]lp\f[], the path of a symlink
|
||||
.IP \[bu] 2
|
||||
\f[B]cc\f[], an escaped character in a filename
|
||||
.IP \[bu] 2
|
||||
\f[B]bO\f[], the overlay style for broken symlink paths
|
||||
.PP
|
||||
Values in \f[C]EXA_COLORS\f[] override those given in
|
||||
\f[C]LS_COLORS\f[], so you don\[aq]t need to re\-write an existing
|
||||
\f[C]LS_COLORS\f[] variable with proprietary extensions.
|
||||
.PP
|
||||
Unlike some versions of \f[C]ls\f[], the given ANSI values must be valid
|
||||
colour codes: exa won\[aq]t just print out whichever characters are
|
||||
given.
|
||||
The codes accepted by exa are:
|
||||
.IP \[bu] 2
|
||||
\f[C]1\f[], for bold
|
||||
.IP \[bu] 2
|
||||
\f[C]4\f[], for underline
|
||||
.IP \[bu] 2
|
||||
\f[C]31\f[], for red text
|
||||
.IP \[bu] 2
|
||||
\f[C]32\f[], for green text
|
||||
.IP \[bu] 2
|
||||
\f[C]33\f[], for yellow text
|
||||
.IP \[bu] 2
|
||||
\f[C]34\f[], for blue text
|
||||
.IP \[bu] 2
|
||||
\f[C]35\f[], for purple text
|
||||
.IP \[bu] 2
|
||||
\f[C]36\f[], for cyan text
|
||||
.IP \[bu] 2
|
||||
\f[C]37\f[], for white text
|
||||
.IP \[bu] 2
|
||||
\f[C]38;5;\f[]\f[I]\f[C]nnn\f[]\f[], for a colour from 0 to 255 (replace
|
||||
the \f[I]nnn\f[] part)
|
||||
.PP
|
||||
Many terminals will treat bolded text as a different colour, or at least
|
||||
provide the option to.
|
||||
.PP
|
||||
exa provides its own built\-in set of file extension mappings that cover
|
||||
a large range of common file extensions, including documents, archives,
|
||||
media, and temporary files.
|
||||
Any mappings in the environment variables will override this default
|
||||
set: running exa with \f[C]LS_COLORS="*.zip=32"\f[] will turn zip files
|
||||
green but leave the colours of other compressed files alone.
|
||||
.PP
|
||||
You can also disable this built\-in set entirely by including a
|
||||
\f[C]reset\f[] entry at the beginning of \f[C]EXA_COLORS\f[].
|
||||
So setting \f[C]EXA_COLORS="reset:*.txt=31"\f[] will highlight only text
|
||||
files; setting \f[C]EXA_COLORS="reset"\f[] will highlight nothing.
|
||||
.SS Examples
|
||||
.IP \[bu] 2
|
||||
Disable the "current user" highlighting: \f[C]EXA_COLORS="uu=0:gu=0"\f[]
|
||||
.IP \[bu] 2
|
||||
Turn the date column green: \f[C]EXA_COLORS="da=32"\f[]
|
||||
.IP \[bu] 2
|
||||
Highlight Vagrantfiles: \f[C]EXA_COLORS="Vagrantfile=1;4;33"\f[]
|
||||
.IP \[bu] 2
|
||||
Override the existing zip colour: \f[C]EXA_COLORS="*.zip=38;5;125"\f[]
|
||||
.IP \[bu] 2
|
||||
Markdown files a shade of green, log files a shade of grey:
|
||||
\f[C]EXA_COLORS="*.md=38;5;121:*.log=38;5;248"\f[]
|
||||
.SS BUILT\-IN EXTENSIONS
|
||||
.IP \[bu] 2
|
||||
"Immediate" files are the files you should look at when downloading and
|
||||
building a project for the first time: READMEs, Makefiles, Cargo.toml,
|
||||
and others.
|
||||
They\[aq]re highlighted in yellow and underlined.
|
||||
.IP \[bu] 2
|
||||
Images (png, jpeg, gif) are purple.
|
||||
.IP \[bu] 2
|
||||
Videos (mp4, ogv, m2ts) are a slightly purpler purple.
|
||||
.IP \[bu] 2
|
||||
Music (mp3, m4a, ogg) is a deeper purple.
|
||||
.IP \[bu] 2
|
||||
Lossless music (flac, alac, wav) is deeper than \f[I]that\f[] purple.
|
||||
In general, most media files are some shade of purple.
|
||||
.IP \[bu] 2
|
||||
Cryptographic files (asc, enc, p12) are a faint blue.
|
||||
.IP \[bu] 2
|
||||
Documents (pdf, doc, dvi) are a less faint blue.
|
||||
.IP \[bu] 2
|
||||
Compressed files (zip, tgz, Z) are red.
|
||||
.IP \[bu] 2
|
||||
Temporary files (tmp, swp, ~) are grey.
|
||||
.IP \[bu] 2
|
||||
Compiled files (class, o, pyc) are faint orange.
|
||||
A file is also counted as compiled if it uses a common extension and is
|
||||
in the same directory as one of its source files: \[aq]styles.css\[aq]
|
||||
will count as compiled when next to \[aq]styles.less\[aq] or
|
||||
\[aq]styles.sass\[aq], and \[aq]scripts.js\[aq] when next to
|
||||
\[aq]scripts.ts\[aq] or \[aq]scripts.coffee\[aq].
|
||||
.SH AUTHOR
|
||||
.PP
|
||||
\f[C]exa\f[] is maintained by Benjamin \[aq]ogham\[aq] Sago and many
|
||||
other contributors.
|
||||
You can view the full list at
|
||||
<https://github.com/ogham/exa/graphs/contributors>.
|
|
@ -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.
|
||||
|
|
|
@ -11,35 +11,42 @@ bash /vagrant/devtools/dev-versions.sh
|
|||
# Configure the Cool Prompt™ (not actually trademarked).
|
||||
# The Cool Prompt tells you whether you’re in debug or strict mode, whether
|
||||
# you have colours configured, and whether your last command failed.
|
||||
function nonzero_return() { RETVAL=$?; [ $RETVAL -ne 0 ] && echo "$RETVAL "; }
|
||||
function debug_mode() { [ -n "$EXA_DEBUG" ] && echo "debug "; }
|
||||
function strict_mode() { [ -n "$EXA_STRICT" ] && echo "strict "; }
|
||||
function lsc_mode() { [ -n "$LS_COLORS" ] && echo "lsc "; }
|
||||
function exac_mode() { [ -n "$EXA_COLORS" ] && echo "exac "; }
|
||||
nonzero_return() { RETVAL=$?; [ "$RETVAL" -ne 0 ] && echo "$RETVAL "; }
|
||||
debug_mode() { [ "$EXA_DEBUG" == "trace" ] && echo -n "trace-"; [ -n "$EXA_DEBUG" ] && echo "debug "; }
|
||||
strict_mode() { [ -n "$EXA_STRICT" ] && echo "strict "; }
|
||||
lsc_mode() { [ -n "$LS_COLORS" ] && echo "lsc "; }
|
||||
exac_mode() { [ -n "$EXA_COLORS" ] && echo "exac "; }
|
||||
export PS1="\[\e[1;36m\]\h \[\e[32m\]\w \[\e[31m\]\`nonzero_return\`\[\e[35m\]\`debug_mode\`\[\e[32m\]\`lsc_mode\`\[\e[1;32m\]\`exac_mode\`\[\e[33m\]\`strict_mode\`\[\e[36m\]\\$\[\e[0m\] "
|
||||
|
||||
|
||||
# The ‘debug’ function lets you switch debug mode on and off.
|
||||
# Turn it on if you need to see exa’s debugging logs.
|
||||
function debug () {
|
||||
case "$1" in "on") export EXA_DEBUG=1 ;;
|
||||
"off") export EXA_DEBUG= ;;
|
||||
"") [ -n "$EXA_DEBUG" ] && echo "debug on" || echo "debug off" ;;
|
||||
*) echo "Usage: debug on|off"; return 1 ;; esac; }
|
||||
debug() {
|
||||
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.
|
||||
function strict () {
|
||||
case "$1" in "on") export EXA_STRICT=1 ;;
|
||||
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; }
|
||||
"") [ -n "$EXA_STRICT" ] && echo "strict on" || echo "strict off" ;;
|
||||
*) echo "Usage: strict on|off"; return 1 ;;
|
||||
esac;
|
||||
}
|
||||
|
||||
# The ‘colors’ function sets or unsets the ‘LS_COLORS’ and ‘EXA_COLORS’
|
||||
# environment variables. There’s also a ‘hacker’ theme which turns everything
|
||||
# green, which is usually used for checking that all colour codes work, and
|
||||
# for looking cool while you phreak some mainframes or whatever.
|
||||
function colors () {
|
||||
colors() {
|
||||
case "$1" in
|
||||
"ls")
|
||||
export LS_COLORS="di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43"
|
||||
|
@ -53,4 +60,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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,377 @@
|
|||
#!/bin/bash
|
||||
# This script creates a bunch of awkward test case files. It gets
|
||||
# automatically run as part of Vagrant provisioning.
|
||||
trap 'exit' ERR
|
||||
|
||||
if [[ ! -d "/vagrant" ]]; then
|
||||
echo "This script should be run in the Vagrant environment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source "/vagrant/devtools/dev-fixtures.sh"
|
||||
|
||||
|
||||
# Delete old testcases if they exist already, then create a
|
||||
# directory to house new ones.
|
||||
if [[ -d "$TEST_ROOT" ]]; then
|
||||
echo -e "\033[1m[ 0/13]\033[0m Deleting existing test cases directory"
|
||||
sudo rm -rf "$TEST_ROOT"
|
||||
fi
|
||||
|
||||
sudo mkdir "$TEST_ROOT"
|
||||
sudo chmod 777 "$TEST_ROOT"
|
||||
sudo mkdir "$TEST_ROOT/empty"
|
||||
|
||||
|
||||
# Awkward file size testcases.
|
||||
# This needs sudo to set the files’ users at the very end.
|
||||
mkdir "$TEST_ROOT/files"
|
||||
echo -e "\033[1m[ 1/13]\033[0m Creating file size testcases"
|
||||
for i in {1..13}; do
|
||||
fallocate -l "$i" "$TEST_ROOT/files/$i"_bytes
|
||||
fallocate -l "$i"KiB "$TEST_ROOT/files/$i"_KiB
|
||||
fallocate -l "$i"MiB "$TEST_ROOT/files/$i"_MiB
|
||||
done
|
||||
|
||||
touch -t $FIXED_DATE "$TEST_ROOT/files/"*
|
||||
touch -t $FIXED_DATE "$TEST_ROOT/files/"
|
||||
chmod 644 "$TEST_ROOT/files/"*
|
||||
sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT/files/"*
|
||||
|
||||
|
||||
# File name extension testcases.
|
||||
# These aren’t tested in details view, but we set timestamps on them to
|
||||
# test that various sort options work.
|
||||
mkdir "$TEST_ROOT/file-names-exts"
|
||||
echo -e "\033[1m[ 2/13]\033[0m Creating file name extension testcases"
|
||||
|
||||
touch "$TEST_ROOT/file-names-exts/Makefile"
|
||||
|
||||
touch "$TEST_ROOT/file-names-exts/IMAGE.PNG"
|
||||
touch "$TEST_ROOT/file-names-exts/image.svg"
|
||||
|
||||
touch "$TEST_ROOT/file-names-exts/VIDEO.AVI"
|
||||
touch "$TEST_ROOT/file-names-exts/video.wmv"
|
||||
|
||||
touch "$TEST_ROOT/file-names-exts/music.mp3"
|
||||
touch "$TEST_ROOT/file-names-exts/MUSIC.OGG"
|
||||
|
||||
touch "$TEST_ROOT/file-names-exts/lossless.flac"
|
||||
touch "$TEST_ROOT/file-names-exts/lossless.wav"
|
||||
|
||||
touch "$TEST_ROOT/file-names-exts/crypto.asc"
|
||||
touch "$TEST_ROOT/file-names-exts/crypto.signature"
|
||||
|
||||
touch "$TEST_ROOT/file-names-exts/document.pdf"
|
||||
touch "$TEST_ROOT/file-names-exts/DOCUMENT.XLSX"
|
||||
|
||||
touch "$TEST_ROOT/file-names-exts/COMPRESSED.ZIP"
|
||||
touch "$TEST_ROOT/file-names-exts/compressed.tar.gz"
|
||||
touch "$TEST_ROOT/file-names-exts/compressed.tgz"
|
||||
touch "$TEST_ROOT/file-names-exts/compressed.tar.xz"
|
||||
touch "$TEST_ROOT/file-names-exts/compressed.txz"
|
||||
touch "$TEST_ROOT/file-names-exts/compressed.deb"
|
||||
|
||||
touch "$TEST_ROOT/file-names-exts/backup~"
|
||||
touch "$TEST_ROOT/file-names-exts/#SAVEFILE#"
|
||||
touch "$TEST_ROOT/file-names-exts/file.tmp"
|
||||
|
||||
touch "$TEST_ROOT/file-names-exts/compiled.class"
|
||||
touch "$TEST_ROOT/file-names-exts/compiled.o"
|
||||
touch "$TEST_ROOT/file-names-exts/compiled.js"
|
||||
touch "$TEST_ROOT/file-names-exts/compiled.coffee"
|
||||
|
||||
|
||||
# File name testcases.
|
||||
# bash really doesn’t want you to create a file with escaped characters
|
||||
# in its name, so we have to resort to the echo builtin and touch!
|
||||
mkdir "$TEST_ROOT/file-names"
|
||||
echo -e "\033[1m[ 3/13]\033[0m Creating file names testcases"
|
||||
|
||||
echo -ne "$TEST_ROOT/file-names/ascii: hello" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/emoji: [🆒]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/utf-8: pâté" | xargs -0 touch
|
||||
|
||||
echo -ne "$TEST_ROOT/file-names/bell: [\a]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/backspace: [\b]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/form-feed: [\f]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/new-line: [\n]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/return: [\r]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/tab: [\t]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/vertical-tab: [\v]" | xargs -0 touch
|
||||
|
||||
echo -ne "$TEST_ROOT/file-names/escape: [\033]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/ansi: [\033[34mblue\033[0m]" | xargs -0 touch
|
||||
|
||||
echo -ne "$TEST_ROOT/file-names/invalid-utf8-1: [\xFF]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/invalid-utf8-2: [\xc3\x28]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/invalid-utf8-3: [\xe2\x82\x28]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/invalid-utf8-4: [\xf0\x28\x8c\x28]" | xargs -0 touch
|
||||
|
||||
echo -ne "$TEST_ROOT/file-names/new-line-dir: [\n]" | xargs -0 mkdir
|
||||
echo -ne "$TEST_ROOT/file-names/new-line-dir: [\n]/subfile" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/new-line-dir: [\n]/another: [\n]" | xargs -0 touch
|
||||
echo -ne "$TEST_ROOT/file-names/new-line-dir: [\n]/broken" | xargs -0 touch
|
||||
|
||||
mkdir "$TEST_ROOT/file-names/links"
|
||||
ln -s "$TEST_ROOT/file-names/new-line-dir"*/* "$TEST_ROOT/file-names/links"
|
||||
|
||||
echo -ne "$TEST_ROOT/file-names/new-line-dir: [\n]/broken" | xargs -0 rm
|
||||
|
||||
|
||||
# Special file testcases.
|
||||
mkdir "$TEST_ROOT/specials"
|
||||
echo -e "\033[1m[ 4/13]\033[0m Creating special file kind testcases"
|
||||
|
||||
sudo mknod "$TEST_ROOT/specials/block-device" b 3 60
|
||||
sudo mknod "$TEST_ROOT/specials/char-device" c 14 40
|
||||
sudo mknod "$TEST_ROOT/specials/named-pipe" p
|
||||
|
||||
sudo touch -t $FIXED_DATE "$TEST_ROOT/specials/"*
|
||||
|
||||
|
||||
# Awkward symlink testcases.
|
||||
mkdir "$TEST_ROOT/links"
|
||||
echo -e "\033[1m[ 5/13]\033[0m Creating symlink testcases"
|
||||
|
||||
ln -s / "$TEST_ROOT/links/root"
|
||||
ln -s /usr "$TEST_ROOT/links/usr"
|
||||
ln -s nowhere "$TEST_ROOT/links/broken"
|
||||
ln -s /proc/1/root "$TEST_ROOT/links/forbidden"
|
||||
|
||||
touch "$TEST_ROOT/links/some_file"
|
||||
ln -s "$TEST_ROOT/links/some_file" "$TEST_ROOT/links/some_file_absolute"
|
||||
(cd "$TEST_ROOT/links"; ln -s "some_file" "some_file_relative")
|
||||
(cd "$TEST_ROOT/links"; ln -s "." "current_dir")
|
||||
(cd "$TEST_ROOT/links"; ln -s ".." "parent_dir")
|
||||
(cd "$TEST_ROOT/links"; ln -s "itself" "itself")
|
||||
|
||||
|
||||
# Awkward passwd testcases.
|
||||
# sudo is needed for these because we technically aren’t a member
|
||||
# of the groups (because they don’t exist), and chown and chgrp
|
||||
# are smart enough to disallow it!
|
||||
mkdir "$TEST_ROOT/passwd"
|
||||
echo -e "\033[1m[ 6/13]\033[0m Creating user and group testcases"
|
||||
|
||||
touch -t $FIXED_DATE "$TEST_ROOT/passwd/unknown-uid"
|
||||
chmod 644 "$TEST_ROOT/passwd/unknown-uid"
|
||||
sudo chown $FIXED_BAD_UID:$FIXED_USER "$TEST_ROOT/passwd/unknown-uid"
|
||||
|
||||
touch -t $FIXED_DATE "$TEST_ROOT/passwd/unknown-gid"
|
||||
chmod 644 "$TEST_ROOT/passwd/unknown-gid"
|
||||
sudo chown $FIXED_USER:$FIXED_BAD_GID "$TEST_ROOT/passwd/unknown-gid"
|
||||
|
||||
|
||||
# Awkward permission testcases.
|
||||
# Differences in the way ‘chmod’ handles setting ‘setuid’ and ‘setgid’
|
||||
# when you don’t already own the file mean that we need to use ‘sudo’
|
||||
# to change permissions to those.
|
||||
mkdir "$TEST_ROOT/permissions"
|
||||
echo -e "\033[1m[ 7/13]\033[0m Creating file permission testcases"
|
||||
|
||||
mkdir "$TEST_ROOT/permissions/forbidden-directory"
|
||||
chmod 000 "$TEST_ROOT/permissions/forbidden-directory"
|
||||
touch -t $FIXED_DATE "$TEST_ROOT/permissions/forbidden-directory"
|
||||
sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT/permissions/forbidden-directory"
|
||||
|
||||
for perms in 000 001 002 004 010 020 040 100 200 400 644 755 777 1000 1001 2000 2010 4000 4100 7666 7777; do
|
||||
touch "$TEST_ROOT/permissions/$perms"
|
||||
sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT/permissions/$perms"
|
||||
sudo chmod $perms "$TEST_ROOT/permissions/$perms"
|
||||
sudo touch -t $FIXED_DATE "$TEST_ROOT/permissions/$perms"
|
||||
done
|
||||
|
||||
|
||||
# Awkward date and time testcases.
|
||||
mkdir "$TEST_ROOT/dates"
|
||||
echo -e "\033[1m[ 8/13]\033[0m Creating date and time testcases"
|
||||
|
||||
# created dates
|
||||
# there’s no way to touch the created date of a file...
|
||||
# so we have to do this the old-fashioned way!
|
||||
# (and make sure these don't actually get listed)
|
||||
touch -t $FIXED_OLD_DATE "$TEST_ROOT/dates/peach"; sleep 1
|
||||
touch -t $FIXED_MED_DATE "$TEST_ROOT/dates/plum"; sleep 1
|
||||
touch -t $FIXED_NEW_DATE "$TEST_ROOT/dates/pear"
|
||||
|
||||
# modified dates
|
||||
touch -t $FIXED_OLD_DATE -m "$TEST_ROOT/dates/pear"
|
||||
touch -t $FIXED_MED_DATE -m "$TEST_ROOT/dates/peach"
|
||||
touch -t $FIXED_NEW_DATE -m "$TEST_ROOT/dates/plum"
|
||||
|
||||
# accessed dates
|
||||
touch -t $FIXED_OLD_DATE -a "$TEST_ROOT/dates/plum"
|
||||
touch -t $FIXED_MED_DATE -a "$TEST_ROOT/dates/pear"
|
||||
touch -t $FIXED_NEW_DATE -a "$TEST_ROOT/dates/peach"
|
||||
|
||||
sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/dates"
|
||||
|
||||
mkdir "$TEST_ROOT/far-dates"
|
||||
touch -t $FIXED_PAST_DATE "$TEST_ROOT/far-dates/the-distant-past"
|
||||
touch -t $FIXED_FUTURE_DATE "$TEST_ROOT/far-dates/beyond-the-future"
|
||||
|
||||
|
||||
# Awkward extended attribute testcases.
|
||||
# We need to test combinations of various numbers of files *and*
|
||||
# extended attributes in directories. Turns out, the easiest way to
|
||||
# do this is to generate all combinations of files with “one-xattr”
|
||||
# or “two-xattrs” in their name and directories with “empty” or
|
||||
# “one-file” in their name, then just give the right number of
|
||||
# xattrs and children to those.
|
||||
mkdir "$TEST_ROOT/attributes"
|
||||
echo -e "\033[1m[ 9/13]\033[0m Creating extended attribute testcases"
|
||||
|
||||
mkdir "$TEST_ROOT/attributes/files"
|
||||
touch "$TEST_ROOT/attributes/files/"{no-xattrs,one-xattr,two-xattrs}{,_forbidden}
|
||||
|
||||
mkdir "$TEST_ROOT/attributes/dirs"
|
||||
mkdir "$TEST_ROOT/attributes/dirs/"{no-xattrs,one-xattr,two-xattrs}_{empty,one-file,two-files}{,_forbidden}
|
||||
|
||||
setfattr -n user.greeting -v hello "$TEST_ROOT/attributes"/**/*{one-xattr,two-xattrs}*
|
||||
setfattr -n user.another_greeting -v hi "$TEST_ROOT/attributes"/**/*two-xattrs*
|
||||
|
||||
for dir in "$TEST_ROOT/attributes/dirs/"*one-file*; do
|
||||
touch $dir/file-in-question
|
||||
done
|
||||
|
||||
for dir in "$TEST_ROOT/attributes/dirs/"*two-files*; do
|
||||
touch $dir/this-file
|
||||
touch $dir/that-file
|
||||
done
|
||||
|
||||
find "$TEST_ROOT/attributes" -exec touch {} -t $FIXED_DATE \;
|
||||
|
||||
# I want to use the following to test,
|
||||
# but it only works on macos:
|
||||
#chmod +a "$FIXED_USER deny readextattr" "$TEST_ROOT/attributes"/**/*_forbidden
|
||||
|
||||
sudo chmod 000 "$TEST_ROOT/attributes"/**/*_forbidden
|
||||
sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/attributes"
|
||||
|
||||
|
||||
# A sample Git repository
|
||||
# This uses cd because it's easier than telling Git where to go each time
|
||||
echo -e "\033[1m[10/13]\033[0m Creating Git testcases (1/4)"
|
||||
mkdir "$TEST_ROOT/git"
|
||||
cd "$TEST_ROOT/git"
|
||||
git init >/dev/null
|
||||
|
||||
mkdir edits additions moves
|
||||
|
||||
echo "original content" | tee edits/{staged,unstaged,both} >/dev/null
|
||||
echo "this file gets moved" > moves/hither
|
||||
|
||||
git add edits moves
|
||||
git config --global user.email "exa@exa.exa"
|
||||
git config --global user.name "Exa Exa"
|
||||
git commit -m "Automated test commit" >/dev/null
|
||||
|
||||
echo "modifications!" | tee edits/{staged,both} >/dev/null
|
||||
touch additions/{staged,edited}
|
||||
mv moves/{hither,thither}
|
||||
|
||||
git add edits moves additions
|
||||
echo "more modifications!" | tee edits/unstaged edits/both additions/edited >/dev/null
|
||||
touch additions/unstaged
|
||||
|
||||
find "$TEST_ROOT/git" -exec touch {} -t $FIXED_DATE \;
|
||||
sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git"
|
||||
|
||||
|
||||
# A second Git repository
|
||||
# for testing two at once
|
||||
echo -e "\033[1m[11/13]\033[0m Creating Git testcases (2/4)"
|
||||
mkdir -p "$TEST_ROOT/git2/deeply/nested/directory"
|
||||
cd "$TEST_ROOT/git2"
|
||||
git init >/dev/null
|
||||
|
||||
touch "deeply/nested/directory/upd8d"
|
||||
git add "deeply/nested/directory/upd8d"
|
||||
git commit -m "Automated test commit" >/dev/null
|
||||
|
||||
echo "Now with contents" > "deeply/nested/directory/upd8d"
|
||||
touch "deeply/nested/directory/l8st"
|
||||
|
||||
echo -e "target\n*.mp3" > ".gitignore"
|
||||
mkdir "ignoreds"
|
||||
touch "ignoreds/music.mp3"
|
||||
touch "ignoreds/music.m4a"
|
||||
mkdir "ignoreds/nested"
|
||||
touch "ignoreds/nested/70s grove.mp3"
|
||||
touch "ignoreds/nested/funky chicken.m4a"
|
||||
mkdir "ignoreds/nested2"
|
||||
touch "ignoreds/nested2/ievan polkka.mp3"
|
||||
|
||||
mkdir "target"
|
||||
touch "target/another ignored file"
|
||||
|
||||
mkdir "deeply/nested/repository"
|
||||
cd "deeply/nested/repository"
|
||||
git init >/dev/null
|
||||
touch subfile
|
||||
# This file, ‘subfile’, should _not_ be marked as a new file by exa, because
|
||||
# it’s in the sub-repository but hasn’t been added to it. Were the sub-repo not
|
||||
# present, it would be marked as a new file, as the top-level repo knows about
|
||||
# the ‘deeply’ directory.
|
||||
|
||||
find "$TEST_ROOT/git2" -exec touch {} -t $FIXED_DATE \;
|
||||
sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git2"
|
||||
|
||||
|
||||
# A third Git repository
|
||||
# Regression test for https://github.com/ogham/exa/issues/526
|
||||
echo -e "\033[1m[12/13]\033[0m Creating Git testcases (3/4)"
|
||||
mkdir -p "$TEST_ROOT/git3"
|
||||
cd "$TEST_ROOT/git3"
|
||||
git init >/dev/null
|
||||
|
||||
# Create a symbolic link pointing to a non-existing file
|
||||
ln -s aaa/aaa/a b
|
||||
|
||||
# This normally fails with:
|
||||
find "$TEST_ROOT/git3" -exec touch {} -h -t $FIXED_DATE \;
|
||||
sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git3"
|
||||
|
||||
|
||||
# A fourth Git repository
|
||||
# Regression test for https://github.com/ogham/exa/issues/698
|
||||
echo -e "\033[1m[12/13]\033[0m Creating Git testcases (4/4)"
|
||||
mkdir -p "$TEST_ROOT/git4"
|
||||
cd "$TEST_ROOT/git4"
|
||||
git init >/dev/null
|
||||
|
||||
# Create a non UTF-8 file
|
||||
touch 'P'$'\b\211''UUU'
|
||||
|
||||
find "$TEST_ROOT/git4" -exec touch {} -h -t $FIXED_DATE \;
|
||||
sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git4"
|
||||
|
||||
|
||||
# Hidden and dot file testcases.
|
||||
# We need to set the permissions of `.` and `..` because they actually
|
||||
# get displayed in the output here, so this has to come last.
|
||||
echo -e "\033[1m[13/13]\033[0m Creating hidden and dot file testcases"
|
||||
shopt -u dotglob
|
||||
GLOBIGNORE=".:.."
|
||||
|
||||
mkdir "$TEST_ROOT/hiddens"
|
||||
cd "$TEST_ROOT/hiddens"
|
||||
touch "$TEST_ROOT/hiddens/visible"
|
||||
touch "$TEST_ROOT/hiddens/.hidden"
|
||||
touch "$TEST_ROOT/hiddens/..extra-hidden"
|
||||
|
||||
# ./hiddens/
|
||||
touch -t $FIXED_DATE "$TEST_ROOT/hiddens/"*
|
||||
chmod 644 "$TEST_ROOT/hiddens/"*
|
||||
sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT/hiddens/"*
|
||||
|
||||
# .
|
||||
touch -t $FIXED_DATE "$TEST_ROOT/hiddens"
|
||||
chmod 755 "$TEST_ROOT/hiddens"
|
||||
sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT/hiddens"
|
||||
|
||||
# ..
|
||||
sudo touch -t $FIXED_DATE "$TEST_ROOT"
|
||||
sudo chmod 755 "$TEST_ROOT"
|
||||
sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT"
|
|
@ -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"
|
|
@ -0,0 +1,43 @@
|
|||
#!/bin/bash
|
||||
# This file contains the text fixtures — the known, constant data — that are
|
||||
# used when setting up the environment that exa’s tests get run in.
|
||||
|
||||
|
||||
# The directory that all the test files are created under.
|
||||
export TEST_ROOT=/testcases
|
||||
|
||||
|
||||
# Because the timestamps are formatted differently depending on whether
|
||||
# they’re in the current year or not (see `details.rs`), we have to make
|
||||
# sure that the files are created in the current year, so they get shown
|
||||
# in the format we expect.
|
||||
export CURRENT_YEAR=$(date "+%Y")
|
||||
export FIXED_DATE="${CURRENT_YEAR}01011234.56" # 1st January, 12:34:56
|
||||
|
||||
|
||||
# We also need an UID and a GID that are guaranteed to not exist, to
|
||||
# test what happen when they don’t.
|
||||
export FIXED_BAD_UID=666
|
||||
export FIXED_BAD_GID=616
|
||||
|
||||
|
||||
# We create two users that own the test files.
|
||||
#
|
||||
# The first one just owns the ordinary ones, because we don’t want the
|
||||
# test outputs to depend on “vagrant” or “ubuntu” existing.
|
||||
#
|
||||
# The second one has a long name, to test that the file owner column
|
||||
# widens correctly. The benefit of Vagrant is that we don’t need to
|
||||
# set this up on the *actual* system!
|
||||
export FIXED_USER="cassowary"
|
||||
export FIXED_LONG_USER="antidisestablishmentarienism"
|
||||
|
||||
|
||||
# A couple of dates, for date-time testing.
|
||||
export FIXED_OLD_DATE='200303030000.00'
|
||||
export FIXED_MED_DATE='200606152314.29' # the june gets used for fr_FR locale tests
|
||||
export FIXED_NEW_DATE='200912221038.53' # and the december for ja_JP local tests
|
||||
|
||||
# Dates that extend beyond 32-bit timespace.
|
||||
export FIXED_PAST_DATE='170001010000.00'
|
||||
export FIXED_FUTURE_DATE='230001010000.00'
|
|
@ -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
|
||||
"
|
|
@ -9,7 +9,7 @@ set -e
|
|||
|
||||
|
||||
# Linux check!
|
||||
uname=`uname -s`
|
||||
uname=$(uname -s)
|
||||
if [[ "$uname" != "Linux" ]]; then
|
||||
echo "Gotta be on Linux to run this (detected '$uname')!"
|
||||
exit 1
|
||||
|
@ -29,8 +29,8 @@ fi
|
|||
|
||||
# Weekly builds have a bit more information in their version number (see build.rs).
|
||||
if [[ "$1" == "--weekly" ]]; then
|
||||
git_hash=`GIT_DIR=/vagrant/.git git rev-parse --short --verify HEAD`
|
||||
date=`date +"%Y-%m-%d"`
|
||||
git_hash=$(GIT_DIR=/vagrant/.git git rev-parse --short --verify HEAD)
|
||||
date=$(date +"%Y-%m-%d")
|
||||
echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash"
|
||||
else
|
||||
echo "Building exa v$exa_version"
|
||||
|
@ -57,9 +57,10 @@ strip -v "$exa_linux_binary"
|
|||
# the binaries can have consistent names, and it’s still possible to tell
|
||||
# different *downloads* apart.
|
||||
echo -e "\n\033[4mZipping binary...\033[0m"
|
||||
if [[ "$1" == "--weekly" ]]
|
||||
then exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}-${date}-${git_hash}.zip"
|
||||
else exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}.zip"
|
||||
if [[ "$1" == "--weekly" ]]; then
|
||||
exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}-${date}-${git_hash}.zip"
|
||||
else
|
||||
exa_linux_zip="/vagrant/exa-linux-x86_64.zip"
|
||||
fi
|
||||
rm -vf "$exa_linux_zip"
|
||||
zip -j "$exa_linux_zip" "$exa_linux_binary"
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [[ ! -d "/vagrant" ]]; then
|
||||
echo "This script should be run in the Vagrant environment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This script should be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source "/vagrant/devtools/dev-fixtures.sh"
|
||||
|
||||
|
||||
# create our test users
|
||||
|
||||
if id -u $FIXED_USER &>/dev/null; then
|
||||
echo "Normal user already exists"
|
||||
else
|
||||
echo "Creating normal user"
|
||||
useradd $FIXED_USER
|
||||
fi
|
||||
|
||||
if id -u $FIXED_LONG_USER &>/dev/null; then
|
||||
echo "Long user already exists"
|
||||
else
|
||||
echo "Creating long user"
|
||||
useradd $FIXED_LONG_USER
|
||||
fi
|
||||
|
||||
|
||||
# locale generation
|
||||
|
||||
# remove most of this file, it slows down locale-gen
|
||||
if grep -F -q "en_GB.UTF-8 UTF-8" /var/lib/locales/supported.d/en; then
|
||||
echo "Removing existing locales"
|
||||
echo "en_US.UTF-8 UTF-8" > /var/lib/locales/supported.d/en
|
||||
fi
|
||||
|
||||
# uncomment these from the config file
|
||||
if grep -F -q "# fr_FR.UTF-8" /etc/locale.gen; then
|
||||
sed -i '/fr_FR.UTF-8/s/^# //g' /etc/locale.gen
|
||||
fi
|
||||
if grep -F -q "# ja_JP.UTF-8" /etc/locale.gen; then
|
||||
sed -i '/ja_JP.UTF-8/s/^# //g' /etc/locale.gen
|
||||
fi
|
||||
|
||||
# only regenerate locales if the config files are newer than the locale archive
|
||||
if [[ ( /var/lib/locales/supported.d/en -nt /usr/lib/locale/locale-archive ) || \
|
||||
( /etc/locale_gen -nt /usr/lib/locale/locale-archive ) ]]; then
|
||||
locale-gen
|
||||
else
|
||||
echo "Locales already generated"
|
||||
fi
|
|
@ -11,7 +11,7 @@ set -e
|
|||
|
||||
# Virtualising macOS is a legal minefield, so this script is ‘local’ instead
|
||||
# of ‘dev’: I run it from my actual machine, rather than from a VM.
|
||||
uname=`uname -s`
|
||||
uname=$(uname -s)
|
||||
if [[ "$uname" != "Darwin" ]]; then
|
||||
echo "Gotta be on Darwin to run this (detected '$uname')!"
|
||||
exit 1
|
||||
|
@ -36,8 +36,8 @@ fi
|
|||
|
||||
# Weekly builds have a bit more information in their version number (see build.rs).
|
||||
if [[ "$1" == "--weekly" ]]; then
|
||||
git_hash=`GIT_DIR=$exa_root/.git git rev-parse --short --verify HEAD`
|
||||
date=`date +"%Y-%m-%d"`
|
||||
git_hash=$(GIT_DIR=$exa_root/.git git rev-parse --short --verify HEAD)
|
||||
date=$(date +"%Y-%m-%d")
|
||||
echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash"
|
||||
else
|
||||
echo "Building exa v$exa_version"
|
||||
|
@ -65,9 +65,10 @@ echo "strip $exa_macos_binary"
|
|||
# the binaries can have consistent names, and it’s still possible to tell
|
||||
# different *downloads* apart.
|
||||
echo -e "\n\033[4mZipping binary...\033[0m"
|
||||
if [[ "$1" == "--weekly" ]]
|
||||
then exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}-${date}-${git_hash}.zip"
|
||||
else exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}.zip"
|
||||
if [[ "$1" == "--weekly" ]]; then
|
||||
exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}-${date}-${git_hash}.zip"
|
||||
else
|
||||
exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}.zip"
|
||||
fi
|
||||
rm -vf "$exa_macos_zip" | sed 's/^/removing /'
|
||||
zip -j "$exa_macos_zip" "$exa_macos_binary"
|
||||
|
|
|
@ -0,0 +1,266 @@
|
|||
% exa(1) v0.9.0
|
||||
|
||||
<!-- This is the exa(1) man page, written in Markdown. -->
|
||||
<!-- To generate the roff version, run `just man`, -->
|
||||
<!-- and the man page will appear in the ‘target’ directory. -->
|
||||
|
||||
|
||||
NAME
|
||||
====
|
||||
|
||||
exa — a modern replacement for ls
|
||||
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
|
||||
`exa [options] [files...]`
|
||||
|
||||
**exa** is a modern replacement for `ls`.
|
||||
It uses colours for information by default, helping you distinguish between many types of files, such as whether you are the owner, or in the owning group.
|
||||
|
||||
It also has extra features not present in the original `ls`, such as viewing the Git status for a directory, or recursing into directories with a tree view.
|
||||
|
||||
|
||||
EXAMPLES
|
||||
========
|
||||
|
||||
`exa`
|
||||
: Lists the contents of the current directory in a grid.
|
||||
|
||||
`exa --oneline --reverse --sort=size`
|
||||
: Displays a list of files with the largest at the top.
|
||||
|
||||
`exa --long --header --inode --git`
|
||||
: Displays a table of files with a header, showing each file’s metadata, inode, and Git status.
|
||||
|
||||
`exa --long --tree --level=3`
|
||||
: Displays a tree of files, three levels deep, as well as each file’s metadata.
|
||||
|
||||
|
||||
DISPLAY OPTIONS
|
||||
===============
|
||||
|
||||
`-1`, `--oneline`
|
||||
: Display one entry per line.
|
||||
|
||||
`-F`, `--classify`
|
||||
: Display file kind indicators next to file names.
|
||||
|
||||
`-G`, `--grid`
|
||||
: Display entries as a grid (default).
|
||||
|
||||
`-l`, `--long`
|
||||
: Display extended file metadata as a table.
|
||||
|
||||
`-R`, `--recurse`
|
||||
: Recurse into directories.
|
||||
|
||||
`-T`, `--tree`
|
||||
: Recurse into directories as a tree.
|
||||
|
||||
`-x`, `--across`
|
||||
: Sort the grid across, rather than downwards.
|
||||
|
||||
`--color`, `--colour=WHEN`
|
||||
: When to use terminal colours.
|
||||
Valid settings are ‘`always`’, ‘`automatic`’, and ‘`never`’.
|
||||
|
||||
`--color-scale`, `--colour-scale`
|
||||
: Colour file sizes on a scale.
|
||||
|
||||
`--icons`
|
||||
: Display icons next to file names.
|
||||
|
||||
`--no-icons`
|
||||
: Don't display icons. (Always overrides --icons)
|
||||
|
||||
|
||||
FILTERING AND SORTING OPTIONS
|
||||
=============================
|
||||
|
||||
`-a`, `--all`
|
||||
: Show hidden and “dot” files.
|
||||
Use this twice to also show the ‘`.`’ and ‘`..`’ directories.
|
||||
|
||||
`-d`, `--list-dirs`
|
||||
: List directories as regular files, rather than recursing and listing their contents.
|
||||
|
||||
`-L`, `--level=DEPTH`
|
||||
: Limit the depth of recursion.
|
||||
|
||||
`-r`, `--reverse`
|
||||
: Reverse the sort order.
|
||||
|
||||
`-s`, `--sort=SORT_FIELD`
|
||||
: Which field to sort by.
|
||||
|
||||
Valid sort fields are ‘`name`’, ‘`Name`’, ‘`extension`’, ‘`Extension`’, ‘`size`’, ‘`modified`’, ‘`changed`’, ‘`accessed`’, ‘`created`’, ‘`inode`’, ‘`type`’, and ‘`none`’.
|
||||
|
||||
The `modified` sort field has the aliases ‘`date`’, ‘`time`’, and ‘`newest`’, and its reverse order has the aliases ‘`age`’ and ‘`oldest`’.
|
||||
|
||||
Sort fields starting with a capital letter will sort uppercase before lowercase: ‘A’ then ‘B’ then ‘a’ then ‘b’. Fields starting with a lowercase letter will mix them: ‘A’ then ‘a’ then ‘B’ then ‘b’.
|
||||
|
||||
`-I`, `--ignore-glob=GLOBS`
|
||||
: Glob patterns, pipe-separated, of files to ignore.
|
||||
|
||||
`--git-ignore` [if exa was built with git support]
|
||||
: Do not list files that are ignored by Git.
|
||||
|
||||
`--group-directories-first`
|
||||
: List directories before other files.
|
||||
|
||||
`-D`, `--only-dirs`
|
||||
: List only directories, not files.
|
||||
|
||||
|
||||
LONG VIEW OPTIONS
|
||||
=================
|
||||
|
||||
These options are available when running with `--long` (`-l`):
|
||||
|
||||
`-b`, `--binary`
|
||||
: List file sizes with binary prefixes.
|
||||
|
||||
`-B`, `--bytes`
|
||||
: List file sizes in bytes, without any prefixes.
|
||||
|
||||
`--changed`
|
||||
: Use the changed timestamp field.
|
||||
|
||||
`-g`, `--group`
|
||||
: List each file’s group.
|
||||
|
||||
`-h`, `--header`
|
||||
: Add a header row to each column.
|
||||
|
||||
`-H`, `--links`
|
||||
: List each file’s number of hard links.
|
||||
|
||||
`-i`, `--inode`
|
||||
: List each file’s inode number.
|
||||
|
||||
`-m`, `--modified`
|
||||
: Use the modified timestamp field.
|
||||
|
||||
`-n`, `--numeric`
|
||||
: List numeric user and group IDs.
|
||||
|
||||
`-S`, `--blocks`
|
||||
: List each file’s number of file system blocks.
|
||||
|
||||
`-t`, `--time=WORD`
|
||||
: Which timestamp field to list.
|
||||
|
||||
: Valid timestamp fields are ‘`modified`’, ‘`changed`’, ‘`accessed`’, and ‘`created`’.
|
||||
|
||||
`--time-style=STYLE`
|
||||
: How to format timestamps.
|
||||
|
||||
: Valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, and ‘`full-iso`’.
|
||||
|
||||
`-u`, `--accessed`
|
||||
: Use the accessed timestamp field.
|
||||
|
||||
`-U`, `--created`
|
||||
: Use the created timestamp field.
|
||||
|
||||
`--no-permissions`
|
||||
: Suppress the permissions field.
|
||||
|
||||
`--no-filesize`
|
||||
: Suppress the file size field.
|
||||
|
||||
`--no-user`
|
||||
: Suppress the user field.
|
||||
|
||||
`--no-time`
|
||||
: Suppress the time field.
|
||||
|
||||
`-@`, `--extended`
|
||||
: List each file’s extended attributes and sizes.
|
||||
|
||||
`--git` [if exa was built with git support]
|
||||
: List each file’s Git status, if tracked.
|
||||
|
||||
This adds a two-character column indicating the staged and unstaged statuses respectively. The status character can be ‘`-`’ for not modified, ‘`M`’ for a modified file, ‘`N`’ for a new file, ‘`D`’ for deleted, ‘`R`’ for renamed, ‘`T`’ for type-change, ‘`I`’ for ignored, and ‘`U`’ for conflicted.
|
||||
|
||||
Directories will be shown to have the status of their contents, which is how ‘deleted’ is possible: if a directory contains a file that has a certain status, it will be shown to have that status.
|
||||
|
||||
|
||||
ENVIRONMENT VARIABLES
|
||||
=====================
|
||||
|
||||
exa responds to the following environment variables:
|
||||
|
||||
## `COLUMNS`
|
||||
|
||||
Overrides the width of the terminal, in characters.
|
||||
|
||||
For example, ‘`COLUMNS=80 exa`’ will show a grid view with a maximum width of 80 characters.
|
||||
|
||||
This option won’t do anything when exa’s output doesn’t wrap, such as when using the `--long` view.
|
||||
|
||||
## `EXA_STRICT`
|
||||
|
||||
Enables _strict mode_, which will make exa error when two command-line options are incompatible.
|
||||
|
||||
Usually, options can override each other going right-to-left on the command line, so that exa can be given aliases: creating an alias ‘`exa=exa --sort=ext`’ then running ‘`exa --sort=size`’ with that alias will run ‘`exa --sort=ext --sort=size`’, and the sorting specified by the user will override the sorting specified by the alias.
|
||||
|
||||
In strict mode, the two options will not co-operate, and exa will error.
|
||||
|
||||
This option is intended for use with automated scripts and other situations where you want to be certain you’re typing in the right command.
|
||||
|
||||
## `EXA_GRID_ROWS`
|
||||
|
||||
Limits the grid-details view (‘`exa --grid --long`’) so it’s only activated when at least the given number of rows of output would be generated.
|
||||
|
||||
With widescreen displays, it’s possible for the grid to look very wide and sparse, on just one or two lines with none of the columns lining up.
|
||||
By specifying a minimum number of rows, you can only use the view if it’s going to be worth using.
|
||||
|
||||
## `EXA_ICON_SPACING`
|
||||
|
||||
Specifies the number of spaces to print between an icon (see the ‘`--icons`’ option) and its file name.
|
||||
|
||||
Different terminals display icons differently, as they usually take up more than one character width on screen, so there’s no “standard” number of spaces that exa can use to separate an icon from text. One space may place the icon too close to the text, and two spaces may place it too far away. So the choice is left up to the user to configure depending on their terminal emulator.
|
||||
|
||||
## `NO_COLOR`
|
||||
|
||||
Disables colours in the output (regardless of its value). Can be overridden by `--color` option.
|
||||
|
||||
See `https://no-color.org/` for details.
|
||||
|
||||
## `LS_COLORS`, `EXA_COLORS`
|
||||
|
||||
Specifies the colour scheme used to highlight files based on their name and kind, as well as highlighting metadata and parts of the UI.
|
||||
|
||||
For more information on the format of these environment variables, see the `exa_colors(5)` manual page.
|
||||
|
||||
|
||||
EXIT STATUSES
|
||||
=============
|
||||
|
||||
0
|
||||
: If everything goes OK.
|
||||
|
||||
1
|
||||
: If there was an I/O error during operation.
|
||||
|
||||
3
|
||||
: If there was a problem with the command-line arguments.
|
||||
|
||||
|
||||
AUTHOR
|
||||
======
|
||||
|
||||
exa is maintained by Benjamin ‘ogham’ Sago and many other contributors.
|
||||
|
||||
**Website:** `https://the.exa.website/` \
|
||||
**Source code:** `https://github.com/ogham/exa` \
|
||||
**Contributors:** `https://github.com/ogham/exa/graphs/contributors`
|
||||
|
||||
|
||||
SEE ALSO
|
||||
========
|
||||
|
||||
- `exa_colors(5)`
|
|
@ -0,0 +1,282 @@
|
|||
% exa_colors(5) v0.9.0
|
||||
|
||||
<!-- This is the exa_colors(5) man page, written in Markdown. -->
|
||||
<!-- To generate the roff version, run `just man`, -->
|
||||
<!-- and the man page will appear in the ‘target’ directory. -->
|
||||
|
||||
|
||||
NAME
|
||||
====
|
||||
|
||||
exa_colors — customising the file and UI colours of exa
|
||||
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
|
||||
The `EXA_COLORS` environment variable can be used to customise the colours that `exa` uses to highlight file names, file metadata, and parts of the UI.
|
||||
|
||||
You can use the `dircolors` program to generate a script that sets the variable from an input file, or if you don’t mind editing long strings of text, you can just type it out directly. These variables have the following structure:
|
||||
|
||||
- A list of key-value pairs separated by ‘`=`’, such as ‘`*.txt=32`’.
|
||||
- Multiple ANSI formatting codes are separated by ‘`;`’, such as ‘`*.txt=32;1;4`’.
|
||||
- Finally, multiple pairs are separated by ‘`:`’, such as ‘`*.txt=32:*.mp3=1;35`’.
|
||||
|
||||
The key half of the pair can either be a two-letter code or a file glob, and anything that’s not a valid code will be treated as a glob, including keys that happen to be two letters long.
|
||||
|
||||
|
||||
EXAMPLES
|
||||
========
|
||||
|
||||
`EXA_COLORS="uu=0:gu=0"`
|
||||
: Disable the “current user” highlighting
|
||||
|
||||
`EXA_COLORS="da=32"`
|
||||
: Turn the date column green
|
||||
|
||||
`EXA_COLORS="Vagrantfile=1;4;33"`
|
||||
: Highlight Vagrantfiles
|
||||
|
||||
`EXA_COLORS="*.zip=38;5;125"`
|
||||
: Override the existing zip colour
|
||||
|
||||
`EXA_COLORS="*.md=38;5;121:*.log=38;5;248"`
|
||||
: Markdown files a shade of green, log files a shade of grey
|
||||
|
||||
|
||||
LIST OF CODES
|
||||
=============
|
||||
|
||||
`LS_COLORS` can use these ten codes:
|
||||
|
||||
`di`
|
||||
: directories
|
||||
|
||||
`ex`
|
||||
: executable files
|
||||
|
||||
`fi`
|
||||
: regular files
|
||||
|
||||
`pi`
|
||||
: named pipes
|
||||
|
||||
`so`
|
||||
: sockets
|
||||
|
||||
`bd`
|
||||
: block devices
|
||||
|
||||
`cd`
|
||||
: character devices
|
||||
|
||||
`ln`
|
||||
: symlinks
|
||||
|
||||
`or`
|
||||
: symlinks with no target
|
||||
|
||||
|
||||
`EXA_COLORS` can use many more:
|
||||
|
||||
`ur`
|
||||
: the user-read permission bit
|
||||
|
||||
`uw`
|
||||
: the user-write permission bit
|
||||
|
||||
`ux`
|
||||
: the user-execute permission bit for regular files
|
||||
|
||||
`ue`
|
||||
: the user-execute for other file kinds
|
||||
|
||||
`gr`
|
||||
: the group-read permission bit
|
||||
|
||||
`gw`
|
||||
: the group-write permission bit
|
||||
|
||||
`gx`
|
||||
: the group-execute permission bit
|
||||
|
||||
`tr`
|
||||
: the others-read permission bit
|
||||
|
||||
`tw`
|
||||
: the others-write permission bit
|
||||
|
||||
`tx`
|
||||
: the others-execute permission bit
|
||||
|
||||
`su`
|
||||
: setuid, setgid, and sticky permission bits for files
|
||||
|
||||
`sf`
|
||||
: setuid, setgid, and sticky for other file kinds
|
||||
|
||||
`xa`
|
||||
: the extended attribute indicator
|
||||
|
||||
`sn`
|
||||
: the numbers of a file’s size (sets `nb`, `nk`, `nm`, `ng` and `nh`)
|
||||
|
||||
`nb`
|
||||
: the numbers of a file’s size if it is lower than 1 KB/Kib
|
||||
|
||||
`nk`
|
||||
: the numbers of a file’s size if it is between 1 KB/KiB and 1 MB/MiB
|
||||
|
||||
`nm`
|
||||
: the numbers of a file’s size if it is between 1 MB/MiB and 1 GB/GiB
|
||||
|
||||
`ng`
|
||||
: the numbers of a file’s size if it is between 1 GB/GiB and 1 TB/TiB
|
||||
|
||||
`nt`
|
||||
: the numbers of a file’s size if it is 1 TB/TiB or higher
|
||||
|
||||
`sb`
|
||||
: the units of a file’s size (sets `ub`, `uk`, `um`, `ug` and `uh`)
|
||||
|
||||
`ub`
|
||||
: the units of a file’s size if it is lower than 1 KB/Kib
|
||||
|
||||
`uk`
|
||||
: the units of a file’s size if it is between 1 KB/KiB and 1 MB/MiB
|
||||
|
||||
`um`
|
||||
: the units of a file’s size if it is between 1 MB/MiB and 1 GB/GiB
|
||||
|
||||
`ug`
|
||||
: the units of a file’s size if it is between 1 GB/GiB and 1 TB/TiB
|
||||
|
||||
`ut`
|
||||
: the units of a file’s size if it is 1 TB/TiB or higher
|
||||
|
||||
`df`
|
||||
: a device’s major ID
|
||||
|
||||
`ds`
|
||||
: a device’s minor ID
|
||||
|
||||
`uu`
|
||||
: a user that’s you
|
||||
|
||||
`un`
|
||||
: a user that’s someone else
|
||||
|
||||
`gu`
|
||||
: a group that you belong to
|
||||
|
||||
`gn`
|
||||
: a group you aren’t a member of
|
||||
|
||||
`lc`
|
||||
: a number of hard links
|
||||
|
||||
`lm`
|
||||
: a number of hard links for a regular file with at least two
|
||||
|
||||
`ga`
|
||||
: a new flag in Git
|
||||
|
||||
`gm`
|
||||
: a modified flag in Git
|
||||
|
||||
`gd`
|
||||
: a deleted flag in Git
|
||||
|
||||
`gv`
|
||||
: a renamed flag in Git
|
||||
|
||||
`gt`
|
||||
: a modified metadata flag in Git
|
||||
|
||||
`xx`
|
||||
: “punctuation”, including many background UI elements
|
||||
|
||||
`da`
|
||||
: a file’s date
|
||||
|
||||
`in`
|
||||
: a file’s inode number
|
||||
|
||||
`bl`
|
||||
: a file’s number of blocks
|
||||
|
||||
`hd`
|
||||
: the header row of a table
|
||||
|
||||
`lp`
|
||||
: the path of a symlink
|
||||
|
||||
`cc`
|
||||
: an escaped character in a filename
|
||||
|
||||
`bO`
|
||||
: the overlay style for broken symlink paths
|
||||
|
||||
Values in `EXA_COLORS` override those given in `LS_COLORS`, so you don’t need to re-write an existing `LS_COLORS` variable with proprietary extensions.
|
||||
|
||||
|
||||
LIST OF STYLES
|
||||
==============
|
||||
|
||||
Unlike some versions of `ls`, the given ANSI values must be valid colour codes: exa won’t just print out whichever characters are given.
|
||||
|
||||
The codes accepted by exa are:
|
||||
|
||||
`1`
|
||||
: for bold
|
||||
|
||||
`4`
|
||||
: for underline
|
||||
|
||||
`31`
|
||||
: for red text
|
||||
|
||||
`32`
|
||||
: for green text
|
||||
|
||||
`33`
|
||||
: for yellow text
|
||||
|
||||
`34`
|
||||
: for blue text
|
||||
|
||||
`35`
|
||||
: for purple text
|
||||
|
||||
`36`
|
||||
: for cyan text
|
||||
|
||||
`37`
|
||||
: for white text
|
||||
|
||||
`38;5;nnn`
|
||||
: for a colour from 0 to 255 (replace the `nnn` part)
|
||||
|
||||
Many terminals will treat bolded text as a different colour, or at least provide the option to.
|
||||
|
||||
exa provides its own built-in set of file extension mappings that cover a large range of common file extensions, including documents, archives, media, and temporary files.
|
||||
Any mappings in the environment variables will override this default set: running exa with `LS_COLORS="*.zip=32"` will turn zip files green but leave the colours of other compressed files alone.
|
||||
|
||||
You can also disable this built-in set entirely by including a `reset` entry at the beginning of `EXA_COLORS`.
|
||||
So setting `EXA_COLORS="reset:*.txt=31"` will highlight only text files; setting `EXA_COLORS="reset"` will highlight nothing.
|
||||
|
||||
|
||||
AUTHOR
|
||||
======
|
||||
|
||||
exa is maintained by Benjamin ‘ogham’ Sago and many other contributors.
|
||||
|
||||
**Website:** `https://the.exa.website/` \
|
||||
**Source code:** `https://github.com/ogham/exa` \
|
||||
**Contributors:** `https://github.com/ogham/exa/graphs/contributors`
|
||||
|
||||
|
||||
SEE ALSO
|
||||
========
|
||||
|
||||
- `exa(1)`
|
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "1.66.1"
|
|
@ -0,0 +1 @@
|
|||
.snapcraft
|
|
@ -0,0 +1,25 @@
|
|||
name: exa
|
||||
version: 'latest'
|
||||
summary: Replacement for 'ls' written in Rust
|
||||
description: |
|
||||
It uses colours for information by default, helping you distinguish between
|
||||
many types of files, such as whether you are the owner, or in the owning
|
||||
group. It also has extra features not present in the original ls, such as
|
||||
viewing the Git status for a directory, or recursing into directories with a
|
||||
tree view. exa is written in Rust, so it’s small, fast, and portable.
|
||||
|
||||
grade: stable
|
||||
confinement: classic
|
||||
|
||||
apps:
|
||||
exa:
|
||||
command: exa
|
||||
|
||||
parts:
|
||||
exa:
|
||||
plugin: rust
|
||||
source: .
|
||||
stage-packages:
|
||||
- libgit2-24
|
||||
- cmake
|
||||
- libz-dev
|
|
@ -1,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;
|
||||
}
|
257
src/exa.rs
257
src/exa.rs
|
@ -1,257 +0,0 @@
|
|||
#![warn(trivial_casts, trivial_numeric_casts)]
|
||||
#![warn(unused_results)]
|
||||
|
||||
extern crate ansi_term;
|
||||
extern crate datetime;
|
||||
extern crate glob;
|
||||
extern crate libc;
|
||||
extern crate locale;
|
||||
extern crate natord;
|
||||
extern crate num_cpus;
|
||||
extern crate number_prefix;
|
||||
extern crate scoped_threadpool;
|
||||
extern crate term_grid;
|
||||
extern crate unicode_width;
|
||||
extern crate users;
|
||||
extern crate zoneinfo_compiled;
|
||||
extern crate term_size;
|
||||
|
||||
#[cfg(feature="git")] extern crate git2;
|
||||
|
||||
#[macro_use] extern crate lazy_static;
|
||||
#[macro_use] extern crate log;
|
||||
|
||||
|
||||
use std::env::var_os;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::io::{stderr, Write, Result as IOResult};
|
||||
use std::path::{Component, PathBuf};
|
||||
|
||||
use ansi_term::{ANSIStrings, Style};
|
||||
|
||||
use fs::{Dir, File};
|
||||
use fs::feature::ignore::IgnoreCache;
|
||||
use fs::feature::git::GitCache;
|
||||
use options::{Options, Vars};
|
||||
pub use options::vars;
|
||||
pub use options::Misfire;
|
||||
use output::{escape, lines, grid, grid_details, details, 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>,
|
||||
|
||||
/// A cache of git-ignored files.
|
||||
/// This lasts the lifetime of the program too, for the same reason.
|
||||
pub ignore: Option<IgnoreCache>,
|
||||
}
|
||||
|
||||
/// The “real” environment variables type.
|
||||
/// Instead of just calling `var_os` from within the options module,
|
||||
/// the method of looking up environment variables has to be passed in.
|
||||
struct LiveVars;
|
||||
impl Vars for LiveVars {
|
||||
fn get(&self, name: &'static str) -> Option<OsString> {
|
||||
var_os(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Git cache populated with the arguments that are going to be
|
||||
/// listed before they’re actually listed, if the options demand it.
|
||||
fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
|
||||
if options.should_scan_for_git() {
|
||||
Some(args.iter().map(PathBuf::from).collect())
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn ignore_cache(options: &Options) -> Option<IgnoreCache> {
|
||||
use fs::filter::GitIgnore;
|
||||
|
||||
match options.filter.git_ignore {
|
||||
GitIgnore::CheckAndIgnore => Some(IgnoreCache::new()),
|
||||
GitIgnore::Off => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> {
|
||||
pub fn from_args<I>(args: I, writer: &'w mut W) -> Result<Exa<'args, 'w, W>, Misfire>
|
||||
where I: Iterator<Item=&'args OsString> {
|
||||
Options::parse(args, &LiveVars).map(move |(options, mut args)| {
|
||||
debug!("Dir action from arguments: {:#?}", options.dir_action);
|
||||
debug!("Filter from arguments: {:#?}", options.filter);
|
||||
debug!("View from arguments: {:#?}", options.view.mode);
|
||||
|
||||
// List the current directory by default, like ls.
|
||||
// This has to be done here, otherwise git_options won’t see it.
|
||||
if args.is_empty() {
|
||||
args = vec![ OsStr::new(".") ];
|
||||
}
|
||||
|
||||
let git = git_options(&options, &args);
|
||||
let ignore = ignore_cache(&options);
|
||||
Exa { options, writer, args, git, ignore }
|
||||
})
|
||||
}
|
||||
|
||||
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();
|
||||
for file in dir.files(self.options.filter.dot_filter, self.ignore.as_ref()) {
|
||||
match file {
|
||||
Ok(file) => children.push(file),
|
||||
Err((path, e)) => writeln!(stderr(), "[{}: {}]", path.display(), e)?,
|
||||
}
|
||||
};
|
||||
|
||||
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 r = details::Render { dir, files, colours, style, opts, filter, recurse };
|
||||
r.render(self.git.as_ref(), self.ignore.as_ref(), self.writer)
|
||||
}
|
||||
|
||||
Mode::GridDetails(ref opts) => {
|
||||
let grid = &opts.grid;
|
||||
let filter = &self.options.filter;
|
||||
let details = &opts.details;
|
||||
let row_threshold = opts.row_threshold;
|
||||
|
||||
let r = grid_details::Render { dir, files, colours, style, grid, details, filter, row_threshold };
|
||||
r.render(self.git.as_ref(), self.writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
use std::io::{self, Result as IOResult};
|
||||
use crate::fs::feature::git::GitCache;
|
||||
use crate::fs::fields::GitStatus;
|
||||
use std::io;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::slice::Iter as SliceIter;
|
||||
|
||||
use fs::File;
|
||||
use fs::feature::ignore::IgnoreCache;
|
||||
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
|
||||
|
@ -32,27 +35,26 @@ 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
|
||||
/// this directory.
|
||||
pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, ignore: Option<&'ig IgnoreCache>) -> Files<'dir, 'ig> {
|
||||
if let Some(i) = ignore { i.discover_underneath(&self.path); }
|
||||
|
||||
pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, git: Option<&'ig GitCache>, git_ignoring: bool) -> Files<'dir, 'ig> {
|
||||
Files {
|
||||
inner: self.contents.iter(),
|
||||
dir: self,
|
||||
dotfiles: dots.shows_dotfiles(),
|
||||
dots: dots.dots(),
|
||||
ignore,
|
||||
git,
|
||||
git_ignoring,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +86,9 @@ pub struct Files<'dir, 'ig> {
|
|||
/// any files have been listed.
|
||||
dots: DotsNext,
|
||||
|
||||
ignore: Option<&'ig IgnoreCache>,
|
||||
git: Option<&'ig GitCache>,
|
||||
|
||||
git_ignoring: bool,
|
||||
}
|
||||
|
||||
impl<'dir, 'ig> Files<'dir, 'ig> {
|
||||
|
@ -103,18 +107,29 @@ 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 let Some(i) = self.ignore {
|
||||
if i.is_ignored(path) { continue }
|
||||
// Also hide _prefix files on Windows because it's used by old applications
|
||||
// as an alternative to dot-prefix files.
|
||||
#[cfg(windows)]
|
||||
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();
|
||||
if git_status.unstaged == GitStatus::Ignored {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return Some(File::from_args(path.clone(), self.dir, filename)
|
||||
.map_err(|e| (path.clone(), e)))
|
||||
}
|
||||
else {
|
||||
return None
|
||||
}
|
||||
|
||||
return None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +148,6 @@ enum DotsNext {
|
|||
Files,
|
||||
}
|
||||
|
||||
|
||||
impl<'dir, 'ig> Iterator for Files<'dir, 'ig> {
|
||||
type Item = Result<File<'dir>, (PathBuf, io::Error)>;
|
||||
|
||||
|
@ -143,24 +157,26 @@ 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)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum DotFilter {
|
||||
|
||||
/// Shows files, dotfiles, and `.` and `..`.
|
||||
|
@ -174,8 +190,8 @@ pub enum DotFilter {
|
|||
}
|
||||
|
||||
impl Default for DotFilter {
|
||||
fn default() -> DotFilter {
|
||||
DotFilter::JustFiles
|
||||
fn default() -> Self {
|
||||
Self::JustFiles
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,18 +200,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
/// into them and print out their contents. The recurse mode does this by
|
||||
/// having extra output blocks at the end, while the tree mode will show
|
||||
/// directories inline, with their contents immediately underneath.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum DirAction {
|
||||
|
||||
/// This directory should be listed along with the regular files, instead
|
||||
|
@ -39,26 +39,26 @@ 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,
|
||||
Self::List => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The options that determine how to recurse into a directory.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct RecurseOptions {
|
||||
|
||||
/// Whether recursion should be done as a tree or as multiple individual
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
//! Getting the Git status of files and directories.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
#[cfg(target_family = "unix")]
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use git2;
|
||||
use log::*;
|
||||
|
||||
use fs::fields as f;
|
||||
use crate::fs::fields as f;
|
||||
|
||||
|
||||
/// A **Git cache** is assembled based on the user’s input arguments.
|
||||
|
@ -36,9 +39,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 +66,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 +79,6 @@ impl FromIterator<PathBuf> for GitCache {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// A **Git repository** is one we’ve discovered somewhere on the filesystem.
|
||||
pub struct GitRepo {
|
||||
|
||||
|
@ -99,7 +104,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 +114,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 +125,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 +160,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,15 +170,14 @@ impl GitRepo {
|
|||
}
|
||||
};
|
||||
|
||||
match repo.workdir().map(|wd| wd.to_path_buf()) {
|
||||
Some(workdir) => {
|
||||
let contents = Mutex::new(GitContents::Before { repo });
|
||||
Ok(GitRepo { contents, workdir, original_path: path, extra_paths: Vec::new() })
|
||||
},
|
||||
None => {
|
||||
warn!("Repository has no workdir?");
|
||||
Err(path)
|
||||
}
|
||||
if let Some(workdir) = repo.workdir() {
|
||||
let workdir = workdir.to_path_buf();
|
||||
let contents = Mutex::new(GitContents::Before { repo });
|
||||
Ok(Self { contents, workdir, original_path: path, extra_paths: Vec::new() })
|
||||
}
|
||||
else {
|
||||
warn!("Repository has no workdir?");
|
||||
Err(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -181,7 +188,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 {
|
||||
|
@ -201,12 +208,19 @@ fn repo_to_statuses(repo: &git2::Repository, workdir: &Path) -> Git {
|
|||
match repo.statuses(None) {
|
||||
Ok(es) => {
|
||||
for e in es.iter() {
|
||||
#[cfg(target_family = "unix")]
|
||||
let path = workdir.join(Path::new(OsStr::from_bytes(e.path_bytes())));
|
||||
// TODO: handle non Unix systems better:
|
||||
// https://github.com/ogham/exa/issues/698
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
let path = workdir.join(Path::new(e.path().unwrap()));
|
||||
let elem = (path, e.status());
|
||||
statuses.push(elem);
|
||||
}
|
||||
},
|
||||
Err(e) => error!("Error looking up Git statuses: {:?}", e),
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error looking up Git statuses: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Git { statuses }
|
||||
|
@ -217,7 +231,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.
|
||||
|
||||
|
||||
|
@ -236,40 +250,71 @@ impl Git {
|
|||
else { self.file_status(index) }
|
||||
}
|
||||
|
||||
/// Get the status for the file at the given path.
|
||||
/// Get the user-facing status of a file.
|
||||
/// We check the statuses directly applying to a file, and for the ignored
|
||||
/// status we check if any of its parents directories is ignored by git.
|
||||
fn file_status(&self, file: &Path) -> f::Git {
|
||||
let path = reorient(file);
|
||||
self.statuses.iter()
|
||||
.find(|p| p.0.as_path() == path)
|
||||
.map(|&(_, s)| f::Git { staged: index_status(s), unstaged: working_tree_status(s) })
|
||||
.unwrap_or_default()
|
||||
|
||||
let s = self.statuses.iter()
|
||||
.filter(|p| if p.1 == git2::Status::IGNORED {
|
||||
path.starts_with(&p.0)
|
||||
} else {
|
||||
p.0 == 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 }
|
||||
}
|
||||
|
||||
/// Get the combined status for all the files whose paths begin with the
|
||||
/// path that gets passed in. This is used for getting the status of
|
||||
/// directories, which don’t really have an ‘official’ status.
|
||||
/// Get the combined, user-facing status of a directory.
|
||||
/// Statuses are aggregating (for example, a directory is considered
|
||||
/// modified if any file under it has the status modified), except for
|
||||
/// ignored status which applies to files under (for example, a directory
|
||||
/// is considered ignored if one of its parent directories is ignored).
|
||||
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| if p.1 == git2::Status::IGNORED {
|
||||
path.starts_with(&p.0)
|
||||
} else {
|
||||
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/REAMDE.md”, prefixed by the workdir.
|
||||
/// “/vagrant/README.md”, prefixed by the workdir.
|
||||
#[cfg(unix)]
|
||||
fn reorient(path: &Path) -> PathBuf {
|
||||
use std::env::current_dir;
|
||||
// I’m not 100% on this func tbh
|
||||
match current_dir() {
|
||||
Err(_) => Path::new(".").join(&path),
|
||||
Ok(dir) => dir.join(&path),
|
||||
}.canonicalize().unwrap() // errors can be ignored here because they only occur if
|
||||
// the path does not exist / a component is not a folder
|
||||
|
||||
// TODO: I’m not 100% on this func tbh
|
||||
let path = match current_dir() {
|
||||
Err(_) => Path::new(".").join(&path),
|
||||
Ok(dir) => dir.join(&path),
|
||||
};
|
||||
|
||||
path.canonicalize().unwrap_or(path)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn reorient(path: &Path) -> PathBuf {
|
||||
let unc_path = path.canonicalize().unwrap();
|
||||
// On Windows UNC path is returned. We need to strip the prefix for it to work.
|
||||
let normal_path = unc_path.as_os_str().to_str().unwrap().trim_left_matches("\\\\?\\");
|
||||
return PathBuf::from(normal_path);
|
||||
}
|
||||
|
||||
/// The character to display if the file has been modified, but not staged.
|
||||
|
@ -281,6 +326,7 @@ fn working_tree_status(status: git2::Status) -> f::GitStatus {
|
|||
s if s.contains(git2::Status::WT_RENAMED) => f::GitStatus::Renamed,
|
||||
s if s.contains(git2::Status::WT_TYPECHANGE) => f::GitStatus::TypeChange,
|
||||
s if s.contains(git2::Status::IGNORED) => f::GitStatus::Ignored,
|
||||
s if s.contains(git2::Status::CONFLICTED) => f::GitStatus::Conflicted,
|
||||
_ => f::GitStatus::NotModified,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,196 +0,0 @@
|
|||
//! Ignoring globs in `.gitignore` files.
|
||||
//!
|
||||
//! This uses a cache because the file with the globs in might not be the same
|
||||
//! directory that we’re listing!
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use fs::filter::IgnorePatterns;
|
||||
|
||||
|
||||
/// An **ignore cache** holds sets of glob patterns paired with the
|
||||
/// directories that they should be ignored underneath. Believe it or not,
|
||||
/// that’s a valid English sentence.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct IgnoreCache {
|
||||
entries: RwLock<Vec<(PathBuf, IgnorePatterns)>>
|
||||
}
|
||||
|
||||
impl IgnoreCache {
|
||||
pub fn new() -> IgnoreCache {
|
||||
IgnoreCache::default()
|
||||
}
|
||||
|
||||
pub fn discover_underneath(&self, path: &Path) {
|
||||
let mut path = Some(path);
|
||||
let mut entries = self.entries.write().unwrap();
|
||||
|
||||
while let Some(p) = path {
|
||||
if p.components().next().is_none() { break }
|
||||
|
||||
let ignore_file = p.join(".gitignore");
|
||||
if ignore_file.is_file() {
|
||||
debug!("Found a .gitignore file: {:?}", ignore_file);
|
||||
if let Ok(mut file) = File::open(ignore_file) {
|
||||
let mut contents = String::new();
|
||||
|
||||
match file.read_to_string(&mut contents) {
|
||||
Ok(_) => {
|
||||
let patterns = file_lines_to_patterns(contents.lines());
|
||||
entries.push((p.into(), patterns));
|
||||
}
|
||||
Err(e) => debug!("Failed to read a .gitignore: {:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
debug!("Found no .gitignore file at {:?}", ignore_file);
|
||||
}
|
||||
|
||||
path = p.parent();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ignored(&self, suspect: &Path) -> bool {
|
||||
let entries = self.entries.read().unwrap();
|
||||
entries.iter().any(|&(ref base_path, ref patterns)| {
|
||||
if let Ok(suffix) = suspect.strip_prefix(&base_path) {
|
||||
patterns.is_ignored_path(suffix)
|
||||
}
|
||||
else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn file_lines_to_patterns<'a, I>(iter: I) -> IgnorePatterns
|
||||
where I: Iterator<Item=&'a str>
|
||||
{
|
||||
let iter = iter.filter(|el| !el.is_empty());
|
||||
let iter = iter.filter(|el| !el.starts_with('#'));
|
||||
|
||||
// TODO: Figure out if this should trim whitespace or not
|
||||
|
||||
// Errors are currently being ignored... not a good look
|
||||
IgnorePatterns::parse_from_iter(iter).0
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_nothing() {
|
||||
use std::iter::empty;
|
||||
let (patterns, _) = IgnorePatterns::parse_from_iter(empty());
|
||||
assert_eq!(patterns, file_lines_to_patterns(empty()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_some_globs() {
|
||||
let stuff = vec![ "*.mp3", "README.md" ];
|
||||
let reals = vec![ "*.mp3", "README.md" ];
|
||||
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
||||
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_some_comments() {
|
||||
let stuff = vec![ "*.mp3", "# I am a comment!", "#", "README.md" ];
|
||||
let reals = vec![ "*.mp3", "README.md" ];
|
||||
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
||||
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_some_blank_lines() {
|
||||
let stuff = vec![ "*.mp3", "", "", "README.md" ];
|
||||
let reals = vec![ "*.mp3", "README.md" ];
|
||||
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
||||
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_some_whitespacey_lines() {
|
||||
let stuff = vec![ " *.mp3", " ", " a ", "README.md " ];
|
||||
let reals = vec![ " *.mp3", " ", " a ", "README.md " ];
|
||||
let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
|
||||
assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
|
||||
}
|
||||
|
||||
|
||||
fn test_cache(dir: &'static str, pats: Vec<&str>) -> IgnoreCache {
|
||||
IgnoreCache { entries: RwLock::new(vec![ (dir.into(), IgnorePatterns::parse_from_iter(pats.into_iter()).0) ]) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_cache_ignores_nothing() {
|
||||
let ignores = IgnoreCache::default();
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/usr/bin/drinking")));
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("target/debug/exa")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_nonempty_cache_ignores_some_things() {
|
||||
let ignores = test_cache("/vagrant", vec![ "target" ]);
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/src")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/target")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_some_globs() {
|
||||
let ignores = test_cache("/vagrant", vec![ "*.ipr", "*.iws", ".docker" ]);
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/exa.ipr")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/exa.iws")));
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/exa.iwiwal")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/.docker")));
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/exa.docker")));
|
||||
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/srcode/exa.ipr")));
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("/srcode/exa.iws")));
|
||||
}
|
||||
|
||||
#[test] #[ignore]
|
||||
fn ignore_relatively() {
|
||||
let ignores = test_cache(".", vec![ "target" ]);
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
||||
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("./.target")));
|
||||
}
|
||||
|
||||
#[test] #[ignore]
|
||||
fn ignore_relatively_sometimes() {
|
||||
let ignores = test_cache(".", vec![ "project/target" ]);
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("./target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
||||
}
|
||||
|
||||
#[test] #[ignore]
|
||||
fn ignore_relatively_absolutely() {
|
||||
let ignores = test_cache(".", vec![ "/project/target" ]);
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("./target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
||||
}
|
||||
|
||||
#[test] #[ignore] // not 100% sure if dot works this way...
|
||||
fn ignore_relatively_absolutely_dot() {
|
||||
let ignores = test_cache(".", vec![ "./project/target" ]);
|
||||
assert_eq!(false, ignores.is_ignored(Path::new("./target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
|
||||
assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
|
||||
}
|
||||
}
|
|
@ -1,21 +1,23 @@
|
|||
pub mod xattr;
|
||||
pub mod ignore;
|
||||
|
||||
#[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};
|
||||
|
||||
use fs::fields as f;
|
||||
use crate::fs::fields as f;
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +27,7 @@ pub mod git {
|
|||
}
|
||||
|
||||
pub fn get(&self, _index: &Path, _prefix_lookup: bool) -> f::Git {
|
||||
panic!("Tried to query a Git cache, but Git support is disabled")
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
//! Extended attribute support for Darwin and Linux systems.
|
||||
#![allow(trivial_casts)] // for ARM
|
||||
extern crate libc;
|
||||
|
||||
#![allow(trivial_casts)] // for ARM
|
||||
|
||||
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!(any(target_os = "macos", target_os = "linux"));
|
||||
|
||||
|
||||
pub trait FileAttributes {
|
||||
fn attributes(&self) -> io::Result<Vec<Attribute>>;
|
||||
|
@ -26,20 +29,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
|
||||
|
@ -49,74 +53,84 @@ 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 mut names = Vec::new();
|
||||
let bufsize = lister.listxattr_first(&c_path);
|
||||
|
||||
if bufsize < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
match bufsize.cmp(&0) {
|
||||
Ordering::Less => return Err(io::Error::last_os_error()),
|
||||
Ordering::Equal => return Ok(Vec::new()),
|
||||
Ordering::Greater => {},
|
||||
}
|
||||
else if bufsize > 0 {
|
||||
let mut buf = vec![0u8; bufsize as usize];
|
||||
let err = lister.listxattr_second(&c_path, &mut buf, bufsize);
|
||||
|
||||
if err < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
let mut buf = vec![0_u8; bufsize as usize];
|
||||
let err = lister.listxattr_second(&c_path, &mut buf, bufsize);
|
||||
|
||||
if err > 0 {
|
||||
// End indicies of the attribute names
|
||||
// the buffer contains 0-terminates c-strings
|
||||
let idx = buf.iter().enumerate().filter_map(|(i, v)|
|
||||
if *v == 0 { Some(i) } else { None }
|
||||
);
|
||||
let mut start = 0;
|
||||
match err.cmp(&0) {
|
||||
Ordering::Less => return Err(io::Error::last_os_error()),
|
||||
Ordering::Equal => return Ok(Vec::new()),
|
||||
Ordering::Greater => {},
|
||||
}
|
||||
|
||||
for end in idx {
|
||||
let c_end = end + 1; // end of the c-string (including 0)
|
||||
let size = lister.getxattr(&c_path, &buf[start..c_end]);
|
||||
let mut names = Vec::new();
|
||||
if err > 0 {
|
||||
// End indices of the attribute names
|
||||
// the buffer contains 0-terminated c-strings
|
||||
let idx = buf.iter().enumerate().filter_map(|(i, v)|
|
||||
if *v == 0 { Some(i) } else { None }
|
||||
);
|
||||
let mut start = 0;
|
||||
|
||||
if size > 0 {
|
||||
names.push(Attribute {
|
||||
name: lister.translate_attribute_name(&buf[start..end]),
|
||||
size: size as usize
|
||||
});
|
||||
}
|
||||
for end in idx {
|
||||
let c_end = end + 1; // end of the c-string (including 0)
|
||||
let size = lister.getxattr(&c_path, &buf[start..c_end]);
|
||||
|
||||
start = c_end;
|
||||
if size > 0 {
|
||||
names.push(Attribute {
|
||||
name: lister.translate_attribute_name(&buf[start..end]),
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -125,26 +139,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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,8 +167,9 @@ mod lister {
|
|||
unsafe {
|
||||
listxattr(
|
||||
c_path.as_ptr(),
|
||||
buf.as_mut_ptr() as *mut c_char,
|
||||
bufsize as size_t, self.c_flags
|
||||
buf.as_mut_ptr().cast::<c_char>(),
|
||||
bufsize as size_t,
|
||||
self.c_flags,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -162,14 +178,18 @@ mod lister {
|
|||
unsafe {
|
||||
getxattr(
|
||||
c_path.as_ptr(),
|
||||
buf.as_ptr() as *const c_char,
|
||||
ptr::null_mut(), 0, 0, self.c_flags
|
||||
buf.as_ptr().cast::<c_char>(),
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
0,
|
||||
self.c_flags,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod lister {
|
||||
use std::ffi::CString;
|
||||
|
@ -179,21 +199,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;
|
||||
}
|
||||
|
||||
|
@ -212,41 +240,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().cast(),
|
||||
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
|
||||
c_path.as_ptr().cast(),
|
||||
buf.as_mut_ptr().cast(),
|
||||
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
|
||||
c_path.as_ptr().cast(),
|
||||
buf.as_ptr().cast(),
|
||||
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
|
||||
|
@ -14,6 +13,7 @@
|
|||
|
||||
// C-style `blkcnt_t` types don’t follow Rust’s rules!
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(clippy::struct_excessive_bools)]
|
||||
|
||||
|
||||
/// The type of a file’s block count.
|
||||
|
@ -43,22 +43,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,
|
||||
|
@ -77,22 +82,44 @@ pub struct Permissions {
|
|||
pub setuid: bool,
|
||||
}
|
||||
|
||||
/// The file's FileAttributes field, available only on Windows.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Attributes {
|
||||
pub archive: bool,
|
||||
pub directory: bool,
|
||||
pub readonly: bool,
|
||||
pub hidden: bool,
|
||||
pub system: bool,
|
||||
pub reparse_point: bool,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
#[cfg(unix)]
|
||||
pub permissions: Permissions,
|
||||
#[cfg(windows)]
|
||||
pub attributes: Attributes,
|
||||
pub xattrs: bool,
|
||||
}
|
||||
|
||||
|
||||
/// The permissions encoded as octal values
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct OctalPermissions {
|
||||
pub permissions: Permissions,
|
||||
}
|
||||
|
||||
/// A file’s number of hard links on the filesystem.
|
||||
///
|
||||
/// Under Unix, a file can exist on the filesystem only once but appear in
|
||||
/// 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.
|
||||
|
@ -106,10 +133,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.
|
||||
|
@ -122,14 +151,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.
|
||||
|
@ -141,10 +173,10 @@ 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
|
||||
/// See this answer for more: <https://unix.stackexchange.com/a/68266>
|
||||
None,
|
||||
|
||||
/// This file is a block or character device, so instead of a size, print
|
||||
|
@ -158,8 +190,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,
|
||||
|
@ -177,6 +210,7 @@ pub struct Time {
|
|||
/// A file’s status in a Git repository. Whether a file is in a repository or
|
||||
/// not is handled by the Git module, rather than having a “null” variant in
|
||||
/// this enum.
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
pub enum GitStatus {
|
||||
|
||||
/// This file hasn’t changed since the last commit.
|
||||
|
@ -200,21 +234,28 @@ pub enum GitStatus {
|
|||
|
||||
/// A file that’s ignored (that matches a line in .gitignore)
|
||||
Ignored,
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
241
src/fs/file.rs
241
src/fs/file.rs
|
@ -1,18 +1,20 @@
|
|||
//! Files, and methods and fields to access their metadata.
|
||||
|
||||
use std::fs::{self, metadata};
|
||||
use std::io::Error as IOError;
|
||||
use std::io::Result as IOResult;
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt};
|
||||
use std::io;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{UNIX_EPOCH, Duration};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use fs::dir::Dir;
|
||||
use fs::fields as f;
|
||||
use options::Misfire;
|
||||
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
|
||||
|
@ -45,8 +47,8 @@ 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.
|
||||
pub metadata: fs::Metadata,
|
||||
/// it’s better to just cache it.
|
||||
pub metadata: std::fs::Metadata,
|
||||
|
||||
/// A reference to the directory that contains this file, if any.
|
||||
///
|
||||
|
@ -61,13 +63,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>>
|
||||
{
|
||||
|
@ -76,31 +78,33 @@ impl<'dir> File<'dir> {
|
|||
let ext = File::ext(&path);
|
||||
|
||||
debug!("Statting file {:?}", &path);
|
||||
let metadata = fs::symlink_metadata(&path)?;
|
||||
let metadata = std::fs::symlink_metadata(&path)?;
|
||||
let is_all_all = false;
|
||||
|
||||
Ok(File { path, parent_dir, metadata, ext, name, is_all_all })
|
||||
Ok(File { name, ext, path, metadata, parent_dir, is_all_all })
|
||||
}
|
||||
|
||||
pub fn new_aa_current(parent_dir: &'dir Dir) -> IOResult<File<'dir>> {
|
||||
let path = parent_dir.path.to_path_buf();
|
||||
pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
|
||||
let path = parent_dir.path.clone();
|
||||
let ext = File::ext(&path);
|
||||
|
||||
debug!("Statting file {:?}", &path);
|
||||
let metadata = fs::symlink_metadata(&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 = fs::symlink_metadata(&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
|
||||
|
@ -128,7 +132,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.
|
||||
|
@ -158,7 +164,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())
|
||||
}
|
||||
|
||||
|
@ -171,6 +177,7 @@ impl<'dir> File<'dir> {
|
|||
/// Whether this file is both a regular file *and* executable for the
|
||||
/// current user. An executable file has a different purpose from an
|
||||
/// executable directory, so they should be highlighted differently.
|
||||
#[cfg(unix)]
|
||||
pub fn is_executable_file(&self) -> bool {
|
||||
let bit = modes::USER_EXECUTE;
|
||||
self.is_file() && (self.metadata.permissions().mode() & bit) == bit
|
||||
|
@ -182,21 +189,25 @@ impl<'dir> File<'dir> {
|
|||
}
|
||||
|
||||
/// Whether this file is a named pipe on the filesystem.
|
||||
#[cfg(unix)]
|
||||
pub fn is_pipe(&self) -> bool {
|
||||
self.metadata.file_type().is_fifo()
|
||||
}
|
||||
|
||||
/// Whether this file is a char device on the filesystem.
|
||||
#[cfg(unix)]
|
||||
pub fn is_char_device(&self) -> bool {
|
||||
self.metadata.file_type().is_char_device()
|
||||
}
|
||||
|
||||
/// Whether this file is a block device on the filesystem.
|
||||
#[cfg(unix)]
|
||||
pub fn is_block_device(&self) -> bool {
|
||||
self.metadata.file_type().is_block_device()
|
||||
}
|
||||
|
||||
/// Whether this file is a socket on the filesystem.
|
||||
#[cfg(unix)]
|
||||
pub fn is_socket(&self) -> bool {
|
||||
self.metadata.file_type().is_socket()
|
||||
}
|
||||
|
@ -210,13 +221,13 @@ impl<'dir> File<'dir> {
|
|||
path.to_path_buf()
|
||||
}
|
||||
else if let Some(dir) = self.parent_dir {
|
||||
dir.join(&*path)
|
||||
dir.join(path)
|
||||
}
|
||||
else if let Some(parent) = self.path.parent() {
|
||||
parent.join(&*path)
|
||||
parent.join(path)
|
||||
}
|
||||
else {
|
||||
self.path.join(&*path)
|
||||
self.path.join(path)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,7 +248,7 @@ impl<'dir> File<'dir> {
|
|||
// we actually look up and turn into a `File` — which needs to be
|
||||
// absolute to be accessible from any directory.
|
||||
debug!("Reading link {:?}", &self.path);
|
||||
let path = match fs::read_link(&self.path) {
|
||||
let path = match std::fs::read_link(&self.path) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return FileTarget::Err(e),
|
||||
};
|
||||
|
@ -246,11 +257,12 @@ impl<'dir> File<'dir> {
|
|||
|
||||
// Use plain `metadata` instead of `symlink_metadata` - we *want* to
|
||||
// follow links.
|
||||
match fs::metadata(&absolute_path) {
|
||||
match std::fs::metadata(&absolute_path) {
|
||||
Ok(metadata) => {
|
||||
let ext = File::ext(&path);
|
||||
let name = File::filename(&path);
|
||||
FileTarget::Ok(Box::new(File { parent_dir: None, path, ext, metadata, name, is_all_all: false }))
|
||||
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);
|
||||
|
@ -266,6 +278,7 @@ impl<'dir> File<'dir> {
|
|||
/// is uncommon, while you come across directories and other types
|
||||
/// with multiple links much more often. Thus, it should get highlighted
|
||||
/// more attentively.
|
||||
#[cfg(unix)]
|
||||
pub fn links(&self) -> f::Links {
|
||||
let count = self.metadata.nlink();
|
||||
|
||||
|
@ -275,14 +288,16 @@ impl<'dir> File<'dir> {
|
|||
}
|
||||
}
|
||||
|
||||
/// This file's inode.
|
||||
/// This file’s inode.
|
||||
#[cfg(unix)]
|
||||
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)
|
||||
#[cfg(unix)]
|
||||
pub fn blocks(&self) -> f::Blocks {
|
||||
if self.is_file() || self.is_link() {
|
||||
f::Blocks::Some(self.metadata.blocks())
|
||||
|
@ -293,11 +308,13 @@ impl<'dir> File<'dir> {
|
|||
}
|
||||
|
||||
/// The ID of the user that own this file.
|
||||
#[cfg(unix)]
|
||||
pub fn user(&self) -> f::User {
|
||||
f::User(self.metadata.uid())
|
||||
}
|
||||
|
||||
/// The ID of the group that owns this file.
|
||||
#[cfg(unix)]
|
||||
pub fn group(&self) -> f::Group {
|
||||
f::Group(self.metadata.gid())
|
||||
}
|
||||
|
@ -310,15 +327,21 @@ impl<'dir> File<'dir> {
|
|||
///
|
||||
/// Block and character devices return their device IDs, because they
|
||||
/// usually just have a file size of zero.
|
||||
#[cfg(unix)]
|
||||
pub fn size(&self) -> f::Size {
|
||||
if self.is_directory() {
|
||||
f::Size::None
|
||||
}
|
||||
else if self.is_char_device() || self.is_block_device() {
|
||||
let dev = self.metadata.rdev();
|
||||
let device_ids = self.metadata.rdev().to_be_bytes();
|
||||
|
||||
// In C-land, getting the major and minor device IDs is done with
|
||||
// preprocessor macros called `major` and `minor` that depend on
|
||||
// the size of `dev_t`, but we just take the second-to-last and
|
||||
// last bytes.
|
||||
f::Size::DeviceIDs(f::DeviceIDs {
|
||||
major: (dev / 256) as u8,
|
||||
minor: (dev % 256) as u8,
|
||||
major: device_ids[6],
|
||||
minor: device_ids[7],
|
||||
})
|
||||
}
|
||||
else {
|
||||
|
@ -326,24 +349,54 @@ impl<'dir> File<'dir> {
|
|||
}
|
||||
}
|
||||
|
||||
/// This file’s last modified timestamp.
|
||||
pub fn modified_time(&self) -> Duration {
|
||||
self.metadata.modified().unwrap().duration_since(UNIX_EPOCH).unwrap()
|
||||
#[cfg(windows)]
|
||||
pub fn size(&self) -> f::Size {
|
||||
if self.is_directory() {
|
||||
f::Size::None
|
||||
}
|
||||
else {
|
||||
f::Size::Some(self.metadata.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// This file’s last changed timestamp.
|
||||
pub fn changed_time(&self) -> Duration {
|
||||
Duration::new(self.metadata.ctime() as u64, self.metadata.ctime_nsec() as u32)
|
||||
/// This file’s last modified timestamp, if available on this platform.
|
||||
pub fn modified_time(&self) -> Option<SystemTime> {
|
||||
self.metadata.modified().ok()
|
||||
}
|
||||
|
||||
/// This file’s last accessed timestamp.
|
||||
pub fn accessed_time(&self) -> Duration {
|
||||
self.metadata.accessed().unwrap().duration_since(UNIX_EPOCH).unwrap()
|
||||
/// This file’s last changed timestamp, if available on this platform.
|
||||
#[cfg(unix)]
|
||||
pub fn changed_time(&self) -> Option<SystemTime> {
|
||||
let (mut sec, mut nanosec) = (self.metadata.ctime(), self.metadata.ctime_nsec());
|
||||
|
||||
if sec < 0 {
|
||||
if nanosec > 0 {
|
||||
sec += 1;
|
||||
nanosec -= 1_000_000_000;
|
||||
}
|
||||
|
||||
let duration = Duration::new(sec.unsigned_abs(), nanosec.unsigned_abs() as u32);
|
||||
Some(UNIX_EPOCH - duration)
|
||||
}
|
||||
else {
|
||||
let duration = Duration::new(sec as u64, nanosec as u32);
|
||||
Some(UNIX_EPOCH + duration)
|
||||
}
|
||||
}
|
||||
|
||||
/// This file’s created timestamp.
|
||||
pub fn created_time(&self) -> Duration {
|
||||
self.metadata.created().unwrap().duration_since(UNIX_EPOCH).unwrap()
|
||||
#[cfg(windows)]
|
||||
pub fn changed_time(&self) -> Option<SystemTime> {
|
||||
return self.modified_time()
|
||||
}
|
||||
|
||||
/// This file’s last accessed timestamp, if available on this platform.
|
||||
pub fn accessed_time(&self) -> Option<SystemTime> {
|
||||
self.metadata.accessed().ok()
|
||||
}
|
||||
|
||||
/// This file’s created timestamp, if available on this platform.
|
||||
pub fn created_time(&self) -> Option<SystemTime> {
|
||||
self.metadata.created().ok()
|
||||
}
|
||||
|
||||
/// This file’s ‘type’.
|
||||
|
@ -351,6 +404,7 @@ impl<'dir> File<'dir> {
|
|||
/// This is used a the leftmost character of the permissions column.
|
||||
/// The file type can usually be guessed from the colour of the file, but
|
||||
/// ls puts this character there.
|
||||
#[cfg(unix)]
|
||||
pub fn type_char(&self) -> f::Type {
|
||||
if self.is_file() {
|
||||
f::Type::File
|
||||
|
@ -378,10 +432,24 @@ impl<'dir> File<'dir> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn type_char(&self) -> f::Type {
|
||||
if self.is_file() {
|
||||
f::Type::File
|
||||
}
|
||||
else if self.is_directory() {
|
||||
f::Type::Directory
|
||||
}
|
||||
else {
|
||||
f::Type::Special
|
||||
}
|
||||
}
|
||||
|
||||
/// This file’s permissions, with flags for each bit.
|
||||
#[cfg(unix)]
|
||||
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),
|
||||
|
@ -402,17 +470,33 @@ impl<'dir> File<'dir> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn attributes(&self) -> f::Attributes {
|
||||
let bits = self.metadata.file_attributes();
|
||||
let has_bit = |bit| bits & bit == bit;
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
|
||||
f::Attributes {
|
||||
directory: has_bit(0x10),
|
||||
archive: has_bit(0x20),
|
||||
readonly: has_bit(0x1),
|
||||
hidden: has_bit(0x2),
|
||||
system: has_bit(0x4),
|
||||
reparse_point: has_bit(0x400),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this file’s extension is any of the strings that get passed in.
|
||||
///
|
||||
/// 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[..])
|
||||
|
@ -440,11 +524,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> {
|
||||
|
@ -452,57 +536,19 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub enum PlatformMetadata {
|
||||
ModifiedTime,
|
||||
ChangedTime,
|
||||
AccessedTime,
|
||||
CreatedTime,
|
||||
}
|
||||
|
||||
impl PlatformMetadata {
|
||||
pub fn check_supported(&self) -> Result<(), Misfire> {
|
||||
use std::env::temp_dir;
|
||||
let result = match self {
|
||||
// Call the functions that return a Result to see if it works
|
||||
PlatformMetadata::AccessedTime => metadata(temp_dir()).unwrap().accessed(),
|
||||
PlatformMetadata::ModifiedTime => metadata(temp_dir()).unwrap().modified(),
|
||||
PlatformMetadata::CreatedTime => metadata(temp_dir()).unwrap().created(),
|
||||
// We use the Unix API so we know it’s not available elsewhere
|
||||
PlatformMetadata::ChangedTime => {
|
||||
if cfg!(target_family = "unix") {
|
||||
return Ok(())
|
||||
} else {
|
||||
return Err(Misfire::Unsupported(
|
||||
// for consistency, this error message similar to the one Rust
|
||||
// use when created time is not available
|
||||
"status modified time is not available on this platform currently".to_string()));
|
||||
}
|
||||
},
|
||||
};
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Misfire::Unsupported(err.to_string()))
|
||||
}
|
||||
matches!(self, Self::Broken(_) | Self::Err(_))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// More readable aliases for the permission bits exposed by libc.
|
||||
#[allow(trivial_numeric_casts)]
|
||||
#[cfg(unix)]
|
||||
mod modes {
|
||||
use libc;
|
||||
|
||||
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;
|
||||
|
@ -575,6 +621,7 @@ mod filename_test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn topmost() {
|
||||
assert_eq!("/", File::filename(Path::new("/")))
|
||||
}
|
||||
|
|
186
src/fs/filter.rs
186
src/fs/filter.rs
|
@ -2,14 +2,11 @@
|
|||
|
||||
use std::cmp::Ordering;
|
||||
use std::iter::FromIterator;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::Path;
|
||||
|
||||
use glob;
|
||||
use natord;
|
||||
|
||||
use fs::File;
|
||||
use fs::DotFilter;
|
||||
use crate::fs::DotFilter;
|
||||
use crate::fs::File;
|
||||
|
||||
|
||||
/// The **file filter** processes a list of files before displaying them to
|
||||
|
@ -26,7 +23,7 @@ use fs::DotFilter;
|
|||
/// The filter also governs sorting the list. After being filtered, pairs of
|
||||
/// files are compared and sorted based on the result, with the sort field
|
||||
/// performing the comparison.
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub struct FileFilter {
|
||||
|
||||
/// Whether directories should be listed first, and other types of file
|
||||
|
@ -53,31 +50,8 @@ pub struct FileFilter {
|
|||
///
|
||||
/// This came about more or less by a complete historical accident,
|
||||
/// when the original `ls` tried to hide `.` and `..`:
|
||||
/// https://plus.google.com/+RobPikeTheHuman/posts/R58WgWwN9jp
|
||||
///
|
||||
/// When one typed ls, however, these files appeared, so either Ken or
|
||||
/// Dennis added a simple test to the program. It was in assembler then,
|
||||
/// but the code in question was equivalent to something like this:
|
||||
/// if (name[0] == '.') continue;
|
||||
/// This statement was a little shorter than what it should have been,
|
||||
/// which is:
|
||||
/// if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
/// but hey, it was easy.
|
||||
///
|
||||
/// Two things resulted.
|
||||
///
|
||||
/// First, a bad precedent was set. A lot of other lazy programmers
|
||||
/// introduced bugs by making the same simplification. Actual files
|
||||
/// beginning with periods are often skipped when they should be counted.
|
||||
///
|
||||
/// Second, and much worse, the idea of a "hidden" or "dot" file was
|
||||
/// created. As a consequence, more lazy programmers started dropping
|
||||
/// files into everyone's home directory. I don't have all that much
|
||||
/// stuff installed on the machine I'm using to type this, but my home
|
||||
/// directory has about a hundred dot files and I don't even know what
|
||||
/// most of them are or whether they're still needed. Every file name
|
||||
/// evaluation that goes through my home directory is slowed down by
|
||||
/// this accumulated sludge.
|
||||
/// [Linux History: How Dot Files Became Hidden Files](https://linux-audit.com/linux-history-how-dot-files-became-hidden-files/)
|
||||
pub dot_filter: DotFilter,
|
||||
|
||||
/// Glob patterns to ignore. Any file name that matches *any* of these
|
||||
|
@ -85,21 +59,17 @@ pub struct FileFilter {
|
|||
pub ignore_patterns: IgnorePatterns,
|
||||
|
||||
/// Whether to ignore Git-ignored patterns.
|
||||
/// This is implemented completely separately from the actual Git
|
||||
/// repository scanning — a `.gitignore` file will still be scanned even
|
||||
/// if there’s no `.git` folder present.
|
||||
pub git_ignore: GitIgnore,
|
||||
}
|
||||
|
||||
|
||||
impl FileFilter {
|
||||
/// Remove every file in the given vector that does *not* pass the
|
||||
/// filter predicate for files found inside a directory.
|
||||
pub fn filter_child_files(&self, files: &mut Vec<File>) {
|
||||
files.retain(|f| !self.ignore_patterns.is_ignored(&f.name));
|
||||
pub fn filter_child_files(&self, files: &mut Vec<File<'_>>) {
|
||||
files.retain(|f| ! self.ignore_patterns.is_ignored(&f.name));
|
||||
|
||||
if self.only_dirs {
|
||||
files.retain(|f| f.is_directory());
|
||||
files.retain(File::is_directory);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,15 +82,19 @@ impl FileFilter {
|
|||
/// dotfile, because it’s been directly specified. But running
|
||||
/// `exa -I='*.ogg' music/*` should filter out the ogg files obtained
|
||||
/// from the glob, even though the globbing is done by the shell!
|
||||
pub fn filter_argument_files(&self, files: &mut Vec<File>) {
|
||||
files.retain(|f| !self.ignore_patterns.is_ignored(&f.name));
|
||||
pub fn filter_argument_files(&self, files: &mut Vec<File<'_>>) {
|
||||
files.retain(|f| {
|
||||
! self.ignore_patterns.is_ignored(&f.name)
|
||||
});
|
||||
}
|
||||
|
||||
/// Sort the files in the given vector based on the sort field option.
|
||||
pub fn sort_files<'a, F>(&self, files: &mut Vec<F>)
|
||||
where F: AsRef<File<'a>> {
|
||||
|
||||
files.sort_by(|a, b| self.sort_field.compare_files(a.as_ref(), b.as_ref()));
|
||||
pub fn sort_files<'a, F>(&self, files: &mut [F])
|
||||
where F: AsRef<File<'a>>
|
||||
{
|
||||
files.sort_by(|a, b| {
|
||||
self.sort_field.compare_files(a.as_ref(), b.as_ref())
|
||||
});
|
||||
|
||||
if self.reverse {
|
||||
files.reverse();
|
||||
|
@ -129,14 +103,17 @@ 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().is_directory().cmp(&a.as_ref().is_directory()));
|
||||
files.sort_by(|a, b| {
|
||||
b.as_ref().points_to_directory()
|
||||
.cmp(&a.as_ref().points_to_directory())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// User-supplied field to sort by.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum SortField {
|
||||
|
||||
/// Don’t apply any sorting. This is usually used as an optimisation in
|
||||
|
@ -154,6 +131,7 @@ pub enum SortField {
|
|||
|
||||
/// The file’s inode, which usually corresponds to the order in which
|
||||
/// files were created on the filesystem, more or less.
|
||||
#[cfg(unix)]
|
||||
FileInode,
|
||||
|
||||
/// The time the file was modified (the “mtime”).
|
||||
|
@ -170,19 +148,19 @@ pub enum SortField {
|
|||
/// slows the whole operation down, so many systems will only update the
|
||||
/// timestamp in certain circumstances. This has become common enough that
|
||||
/// it’s now expected behaviour!
|
||||
/// http://unix.stackexchange.com/a/8842
|
||||
/// <https://unix.stackexchange.com/a/8842>
|
||||
AccessedDate,
|
||||
|
||||
/// The time the file was changed (the “ctime”).
|
||||
///
|
||||
/// This field is used to mark the time when a file’s metadata
|
||||
/// changed -- its permissions, owners, or link count.
|
||||
/// 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
|
||||
/// <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.
|
||||
|
@ -216,7 +194,7 @@ pub enum SortField {
|
|||
/// lowercase letters because it takes the difference between the two cases
|
||||
/// into account? I gave up and just named these two variants after the
|
||||
/// effects they have.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum SortCase {
|
||||
|
||||
/// Sort files case-sensitively with uppercase first, with ‘A’ coming
|
||||
|
@ -237,54 +215,54 @@ impl SortField {
|
|||
/// into groups between letters and numbers, and then sorts those blocks
|
||||
/// together, so `file10` will sort after `file9`, instead of before it
|
||||
/// because of the `1`.
|
||||
pub fn compare_files(self, a: &File, b: &File) -> Ordering {
|
||||
pub fn compare_files(self, a: &File<'_>, b: &File<'_>) -> Ordering {
|
||||
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()),
|
||||
#[cfg(unix)]
|
||||
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
|
||||
match n.strip_prefix('.') {
|
||||
Some(s) => s,
|
||||
None => n,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -293,22 +271,26 @@ impl SortField {
|
|||
/// The **ignore patterns** are a list of globs that are tested against
|
||||
/// each filename, and if any of them match, that file isn’t displayed.
|
||||
/// This lets a user hide, say, text files by ignoring `*.txt`.
|
||||
#[derive(PartialEq, Default, Debug, Clone)]
|
||||
#[derive(PartialEq, Eq, Default, Debug, Clone)]
|
||||
pub struct IgnorePatterns {
|
||||
patterns: Vec<glob::Pattern>,
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
@ -329,48 +311,32 @@ 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.
|
||||
fn is_ignored(&self, file: &str) -> bool {
|
||||
self.patterns.iter().any(|p| p.matches(file))
|
||||
}
|
||||
|
||||
/// Test whether the given file should be hidden from the results.
|
||||
pub fn is_ignored_path(&self, file: &Path) -> bool {
|
||||
self.patterns.iter().any(|p| p.matches_path(file))
|
||||
}
|
||||
|
||||
// TODO(ogham): The fact that `is_ignored_path` is pub while `is_ignored`
|
||||
// isn’t probably means it’s in the wrong place
|
||||
}
|
||||
|
||||
|
||||
/// Whether to ignore or display files that are mentioned in `.gitignore` files.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
/// Whether to ignore or display files that Git would ignore.
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum GitIgnore {
|
||||
|
||||
/// Ignore files that Git would ignore. This means doing a check for a
|
||||
/// `.gitignore` file, possibly recursively up the filesystem tree.
|
||||
/// Ignore files that Git would ignore.
|
||||
CheckAndIgnore,
|
||||
|
||||
/// Display files, even if Git would ignore them.
|
||||
Off,
|
||||
}
|
||||
|
||||
// This is not fully baked yet. The `ignore` crate lists a lot more files that
|
||||
// we aren’t checking:
|
||||
//
|
||||
// > By default, all ignore files found are respected. This includes .ignore,
|
||||
// > .gitignore, .git/info/exclude and even your global gitignore globs,
|
||||
// > usually found in $XDG_CONFIG_HOME/git/ignore.
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -380,31 +346,31 @@ mod test_ignores {
|
|||
#[test]
|
||||
fn empty_matches_nothing() {
|
||||
let pats = IgnorePatterns::empty();
|
||||
assert_eq!(false, pats.is_ignored("nothing"));
|
||||
assert_eq!(false, pats.is_ignored("test.mp3"));
|
||||
assert!(!pats.is_ignored("nothing"));
|
||||
assert!(!pats.is_ignored("test.mp3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_a_glob() {
|
||||
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "*.mp3" ]);
|
||||
assert!(fails.is_empty());
|
||||
assert_eq!(false, pats.is_ignored("nothing"));
|
||||
assert_eq!(true, pats.is_ignored("test.mp3"));
|
||||
assert!(!pats.is_ignored("nothing"));
|
||||
assert!(pats.is_ignored("test.mp3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_an_exact_filename() {
|
||||
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing" ]);
|
||||
assert!(fails.is_empty());
|
||||
assert_eq!(true, pats.is_ignored("nothing"));
|
||||
assert_eq!(false, pats.is_ignored("test.mp3"));
|
||||
assert!(pats.is_ignored("nothing"));
|
||||
assert!(!pats.is_ignored("test.mp3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_both() {
|
||||
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing", "*.mp3" ]);
|
||||
assert!(fails.is_empty());
|
||||
assert_eq!(true, pats.is_ignored("nothing"));
|
||||
assert_eq!(true, pats.is_ignored("test.mp3"));
|
||||
assert!(pats.is_ignored("nothing"));
|
||||
assert!(pats.is_ignored("test.mp3"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ mod dir;
|
|||
pub use self::dir::{Dir, DotFilter};
|
||||
|
||||
mod file;
|
||||
pub use self::file::{File, FileTarget, PlatformMetadata};
|
||||
pub use self::file::{File, FileTarget};
|
||||
|
||||
pub mod dir_action;
|
||||
pub mod feature;
|
||||
pub mod fields;
|
||||
pub mod filter;
|
||||
pub mod dir_action;
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
use ansi_term::Style;
|
||||
|
||||
use fs::File;
|
||||
use output::file_name::FileColours;
|
||||
use output::icons::FileIcon;
|
||||
use crate::fs::File;
|
||||
use crate::output::icons::FileIcon;
|
||||
use crate::theme::FileColours;
|
||||
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct FileExtensions;
|
||||
|
||||
impl FileExtensions {
|
||||
|
@ -19,73 +19,80 @@ impl FileExtensions {
|
|||
/// An “immediate” file is something that can be run or activated somehow
|
||||
/// in order to kick off the build of a project. It’s usually only present
|
||||
/// in directories full of source code.
|
||||
fn is_immediate(&self, file: &File) -> bool {
|
||||
file.name.to_lowercase().starts_with("readme") || file.name_is_one_of( &[
|
||||
#[allow(clippy::case_sensitive_file_extension_comparisons)]
|
||||
fn is_immediate(&self, file: &File<'_>) -> bool {
|
||||
file.name.to_lowercase().starts_with("readme") ||
|
||||
file.name.ends_with(".ninja") ||
|
||||
file.name_is_one_of( &[
|
||||
"Makefile", "Cargo.toml", "SConstruct", "CMakeLists.txt",
|
||||
"build.gradle", "Rakefile", "Gruntfile.js",
|
||||
"Gruntfile.coffee", "BUILD", "WORKSPACE", "build.xml"
|
||||
"build.gradle", "pom.xml", "Rakefile", "package.json", "Gruntfile.js",
|
||||
"Gruntfile.coffee", "BUILD", "BUILD.bazel", "WORKSPACE", "build.xml", "Podfile",
|
||||
"webpack.config.js", "meson.build", "composer.json", "RoboFile.php", "PKGBUILD",
|
||||
"Justfile", "Procfile", "Dockerfile", "Containerfile", "Vagrantfile", "Brewfile",
|
||||
"Gemfile", "Pipfile", "build.sbt", "mix.exs", "bsconfig.json", "tsconfig.json",
|
||||
])
|
||||
}
|
||||
|
||||
fn is_image(&self, file: &File) -> bool {
|
||||
fn is_image(&self, file: &File<'_>) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif",
|
||||
"ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw",
|
||||
"svg", "stl", "eps", "dvi", "ps", "cbr", "jpf",
|
||||
"cbz", "xpm", "ico", "cr2", "orf", "nef",
|
||||
"png", "jfi", "jfif", "jif", "jpe", "jpeg", "jpg", "gif", "bmp",
|
||||
"tiff", "tif", "ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw",
|
||||
"svg", "stl", "eps", "dvi", "ps", "cbr", "jpf", "cbz", "xpm",
|
||||
"ico", "cr2", "orf", "nef", "heif", "avif", "jxl", "j2k", "jp2",
|
||||
"j2c", "jpx",
|
||||
])
|
||||
}
|
||||
|
||||
fn is_video(&self, file: &File) -> bool {
|
||||
fn is_video(&self, file: &File<'_>) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"avi", "flv", "m2v", "m4v", "mkv", "mov", "mp4", "mpeg",
|
||||
"mpg", "ogm", "ogv", "vob", "wmv", "webm", "m2ts",
|
||||
"mpg", "ogm", "ogv", "vob", "wmv", "webm", "m2ts", "heic",
|
||||
])
|
||||
}
|
||||
|
||||
fn is_music(&self, file: &File) -> bool {
|
||||
fn is_music(&self, file: &File<'_>) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"aac", "m4a", "mp3", "ogg", "wma", "mka", "opus",
|
||||
])
|
||||
}
|
||||
|
||||
// Lossless music, rather than any other kind of data...
|
||||
fn is_lossless(&self, file: &File) -> bool {
|
||||
fn is_lossless(&self, file: &File<'_>) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"alac", "ape", "flac", "wav",
|
||||
])
|
||||
}
|
||||
|
||||
fn is_crypto(&self, file: &File) -> bool {
|
||||
fn is_crypto(&self, file: &File<'_>) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"asc", "enc", "gpg", "pgp", "sig", "signature", "pfx", "p12",
|
||||
])
|
||||
}
|
||||
|
||||
fn is_document(&self, file: &File) -> bool {
|
||||
fn is_document(&self, file: &File<'_>) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"djvu", "doc", "docx", "dvi", "eml", "eps", "fotd",
|
||||
"odp", "odt", "pdf", "ppt", "pptx", "rtf",
|
||||
"xls", "xlsx",
|
||||
"djvu", "doc", "docx", "dvi", "eml", "eps", "fotd", "key",
|
||||
"keynote", "numbers", "odp", "odt", "pages", "pdf", "ppt",
|
||||
"pptx", "rtf", "xls", "xlsx",
|
||||
])
|
||||
}
|
||||
|
||||
fn is_compressed(&self, file: &File) -> bool {
|
||||
fn is_compressed(&self, file: &File<'_>) -> bool {
|
||||
file.extension_is_one_of( &[
|
||||
"zip", "tar", "Z", "z", "gz", "bz2", "a", "ar", "7z",
|
||||
"iso", "dmg", "tc", "rar", "par", "tgz", "xz", "txz",
|
||||
"lzma", "deb", "rpm", "zst",
|
||||
"lz", "tlz", "lzma", "deb", "rpm", "zst", "lz4", "cpio",
|
||||
])
|
||||
}
|
||||
|
||||
fn is_temp(&self, file: &File) -> bool {
|
||||
fn is_temp(&self, file: &File<'_>) -> bool {
|
||||
file.name.ends_with('~')
|
||||
|| (file.name.starts_with('#') && file.name.ends_with('#'))
|
||||
|| file.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak", "bk" ])
|
||||
|| file.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak", "bkp", "bk" ])
|
||||
}
|
||||
|
||||
fn is_compiled(&self, file: &File) -> bool {
|
||||
if file.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc" ]) {
|
||||
fn is_compiled(&self, file: &File<'_>) -> bool {
|
||||
if file.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc", "zwc", "ko" ]) {
|
||||
true
|
||||
}
|
||||
else if let Some(dir) = file.parent_dir {
|
||||
|
@ -98,7 +105,7 @@ impl FileExtensions {
|
|||
}
|
||||
|
||||
impl FileColours for FileExtensions {
|
||||
fn colour_file(&self, file: &File) -> Option<Style> {
|
||||
fn colour_file(&self, file: &File<'_>) -> Option<Style> {
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
Some(match file {
|
||||
|
@ -118,14 +125,20 @@ impl FileColours for FileExtensions {
|
|||
}
|
||||
|
||||
impl FileIcon for FileExtensions {
|
||||
fn icon_file(&self, file: &File) -> Option<char> {
|
||||
use output::icons::Icons;
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use fs::File;
|
||||
use crate::fs::File;
|
||||
|
||||
|
||||
impl<'a> File<'a> {
|
||||
|
@ -8,25 +8,24 @@ impl<'a> File<'a> {
|
|||
/// For this file, return a vector of alternate file paths that, if any of
|
||||
/// them exist, mean that *this* file should be coloured as “compiled”.
|
||||
///
|
||||
/// The point of this is to highlight compiled files such as `foo.o` when
|
||||
/// their source file `foo.c` exists in the same directory. It's too
|
||||
/// dangerous to highlight *all* compiled, so the paths in this vector
|
||||
/// are checked for existence first: for example, `foo.js` is perfectly
|
||||
/// valid without `foo.coffee`.
|
||||
/// 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.
|
||||
/// (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[..] {
|
||||
"class" => vec![self.path.with_extension("java")], // Java
|
||||
"css" => vec![self.path.with_extension("sass"), self.path.with_extension("less")], // SASS, Less
|
||||
"elc" => vec![self.path.with_extension("el")], // Emacs Lisp
|
||||
"hi" => vec![self.path.with_extension("hs")], // Haskell
|
||||
"css" => vec![self.path.with_extension("sass"), self.path.with_extension("scss"), // SASS, SCSS
|
||||
self.path.with_extension("styl"), self.path.with_extension("less")], // Stylus, Less
|
||||
"js" => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")], // CoffeeScript, TypeScript
|
||||
"o" => vec![self.path.with_extension("c"), self.path.with_extension("cpp")], // C, C++
|
||||
"pyc" => vec![self.path.with_extension("py")], // Python
|
||||
|
||||
"aux" | // TeX: auxiliary file
|
||||
"bbl" | // BibTeX bibliography file
|
||||
"bcf" | // biblatex control file
|
||||
"blg" | // BibTeX log file
|
||||
"fdb_latexmk" | // TeX latexmk file
|
||||
"fls" | // TeX -recorder file
|
||||
"lof" | // TeX list of figures
|
||||
"log" | // TeX log file
|
||||
"lot" | // TeX list of tables
|
||||
|
@ -36,7 +35,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!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,341 @@
|
|||
#![warn(deprecated_in_future)]
|
||||
#![warn(future_incompatible)]
|
||||
#![warn(nonstandard_style)]
|
||||
#![warn(rust_2018_compatibility)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
#![warn(trivial_casts, trivial_numeric_casts)]
|
||||
#![warn(unused)]
|
||||
|
||||
#![warn(clippy::all, clippy::pedantic)]
|
||||
#![allow(clippy::cast_precision_loss)]
|
||||
#![allow(clippy::cast_possible_truncation)]
|
||||
#![allow(clippy::cast_possible_wrap)]
|
||||
#![allow(clippy::cast_sign_loss)]
|
||||
#![allow(clippy::enum_glob_use)]
|
||||
#![allow(clippy::map_unwrap_or)]
|
||||
#![allow(clippy::match_same_arms)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
#![allow(clippy::non_ascii_literal)]
|
||||
#![allow(clippy::option_if_let_else)]
|
||||
#![allow(clippy::too_many_lines)]
|
||||
#![allow(clippy::unused_self)]
|
||||
#![allow(clippy::upper_case_acronyms)]
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
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};
|
||||
use crate::theme::Theme;
|
||||
|
||||
mod fs;
|
||||
mod info;
|
||||
mod logger;
|
||||
mod options;
|
||||
mod output;
|
||||
mod theme;
|
||||
|
||||
|
||||
fn main() {
|
||||
use std::process::exit;
|
||||
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||
}
|
||||
|
||||
logger::configure(env::var_os(vars::EXA_DEBUG));
|
||||
|
||||
#[cfg(windows)]
|
||||
if let Err(e) = ansi_term::enable_ansi_support() {
|
||||
warn!("Failed to enable ANSI support: {}", e);
|
||||
}
|
||||
|
||||
let args: Vec<_> = env::args_os().skip(1).collect();
|
||||
match Options::parse(args.iter().map(std::convert::AsRef::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 console_width = options.view.width.actual_terminal_width();
|
||||
let theme = options.theme.to_theme(console_width.is_some());
|
||||
let exa = Exa { options, writer, input_paths, theme, console_width, 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) => {
|
||||
print!("{}", help_text);
|
||||
}
|
||||
|
||||
OptionsResult::Version(version_str) => {
|
||||
print!("{}", version_str);
|
||||
}
|
||||
|
||||
OptionsResult::InvalidOptions(error) => {
|
||||
eprintln!("exa: {}", 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>,
|
||||
|
||||
/// The theme that has been configured from the command-line options and
|
||||
/// environment variables. If colours are disabled, this is a theme with
|
||||
/// every style set to the default.
|
||||
pub theme: Theme,
|
||||
|
||||
/// The detected width of the console. This is used to determine which
|
||||
/// view to use.
|
||||
pub console_width: Option<usize>,
|
||||
|
||||
/// 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> {
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if printing to stderr fails.
|
||||
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.
|
||||
fn print_files(&mut self, dir: Option<&Dir>, files: Vec<File<'_>>) -> io::Result<()> {
|
||||
if files.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let theme = &self.theme;
|
||||
let View { ref mode, ref file_style, .. } = self.options.view;
|
||||
|
||||
match (mode, self.console_width) {
|
||||
(Mode::Grid(ref opts), Some(console_width)) => {
|
||||
let filter = &self.options.filter;
|
||||
let r = grid::Render { files, theme, file_style, opts, console_width, filter };
|
||||
r.render(&mut self.writer)
|
||||
}
|
||||
|
||||
(Mode::Grid(_), None) |
|
||||
(Mode::Lines, _) => {
|
||||
let filter = &self.options.filter;
|
||||
let r = lines::Render { files, theme, file_style, filter };
|
||||
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, theme, file_style, opts, recurse, filter, git_ignoring, git };
|
||||
r.render(&mut self.writer)
|
||||
}
|
||||
|
||||
(Mode::GridDetails(ref opts), Some(console_width)) => {
|
||||
let grid = &opts.grid;
|
||||
let details = &opts.details;
|
||||
let row_threshold = opts.row_threshold;
|
||||
|
||||
let filter = &self.options.filter;
|
||||
let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
|
||||
let git = self.git.as_ref();
|
||||
|
||||
let r = grid_details::Render { dir, files, theme, file_style, grid, details, filter, row_threshold, git_ignoring, git, console_width };
|
||||
r.render(&mut self.writer)
|
||||
}
|
||||
|
||||
(Mode::GridDetails(ref opts), None) => {
|
||||
let opts = &opts.to_details_options();
|
||||
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, theme, file_style, opts, recurse, filter, 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,9 +1,9 @@
|
|||
//! Parsing the options for `DirAction`.
|
||||
|
||||
use options::parser::MatchedFlags;
|
||||
use options::{flags, Misfire};
|
||||
use crate::options::parser::MatchedFlags;
|
||||
use crate::options::{flags, OptionsError, NumberSource};
|
||||
|
||||
use fs::dir_action::{DirAction, RecurseOptions};
|
||||
use crate::fs::dir_action::{DirAction, RecurseOptions};
|
||||
|
||||
|
||||
impl DirAction {
|
||||
|
@ -12,35 +12,37 @@ 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<'_>, can_tree: bool) -> 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)?))
|
||||
if tree && can_tree {
|
||||
// Tree is only appropriate in details mode, so this has to
|
||||
// examine the View, which should have already been deduced by now
|
||||
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 +54,22 @@ 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> {
|
||||
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)),
|
||||
pub fn deduce(matches: &MatchedFlags<'_>, tree: bool) -> Result<Self, OptionsError> {
|
||||
if let Some(level) = matches.get(&flags::LEVEL)? {
|
||||
let arg_str = level.to_string_lossy();
|
||||
match arg_str.parse() {
|
||||
Ok(l) => {
|
||||
Ok(Self { tree, max_depth: Some(l) })
|
||||
}
|
||||
Err(e) => {
|
||||
let source = NumberSource::Arg(&flags::LEVEL);
|
||||
Err(OptionsError::FailedParse(arg_str.to_string(), source, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(RecurseOptions { tree, max_depth })
|
||||
Ok(Self { tree, max_depth: None })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,19 +77,19 @@ impl RecurseOptions {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use options::flags;
|
||||
use options::parser::Flag;
|
||||
use crate::options::flags;
|
||||
use crate::options::parser::Flag;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
use options::parser::Arg;
|
||||
use options::test::parse_for_test;
|
||||
use options::test::Strictnesses::*;
|
||||
use crate::options::parser::Arg;
|
||||
use crate::options::test::parse_for_test;
|
||||
use crate::options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[&flags::RECURSE, &flags::LIST_DIRS, &flags::TREE, &flags::LEVEL ];
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, true)) {
|
||||
assert_eq!(result, $result);
|
||||
}
|
||||
}
|
||||
|
@ -115,12 +121,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'))));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
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, Eq, 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 that are unsupported
|
||||
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(String, NumberSource, ParseIntError),
|
||||
|
||||
/// A glob ignore was given that failed to be parsed as a pattern.
|
||||
FailedGlobPattern(String),
|
||||
}
|
||||
|
||||
/// The source of a string that failed to be parsed as a number.
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum NumberSource {
|
||||
|
||||
/// It came... from a command-line argument!
|
||||
Arg(&'static Arg),
|
||||
|
||||
/// It came... from the enviroment!
|
||||
Env(&'static str),
|
||||
}
|
||||
|
||||
impl From<glob::PatternError> for OptionsError {
|
||||
fn from(error: glob::PatternError) -> Self {
|
||||
Self::FailedGlobPattern(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for NumberSource {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Arg(arg) => write!(f, "option {}", arg),
|
||||
Self::Env(env) => write!(f, "environment variable {}", env),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(s, n, e) => write!(f, "Value {:?} not valid for {}: {}", s, n, 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, Eq, 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(", "))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
use crate::options::{flags, OptionsError, NumberSource};
|
||||
use crate::options::parser::MatchedFlags;
|
||||
use crate::options::vars::{self, Vars};
|
||||
|
||||
use crate::output::file_name::{Options, Classify, ShowIcons};
|
||||
|
||||
|
||||
impl Options {
|
||||
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
|
||||
let classify = Classify::deduce(matches)?;
|
||||
let show_icons = ShowIcons::deduce(matches, vars)?;
|
||||
|
||||
Ok(Self { classify, show_icons })
|
||||
}
|
||||
}
|
||||
|
||||
impl Classify {
|
||||
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
|
||||
let flagged = matches.has(&flags::CLASSIFY)?;
|
||||
|
||||
if flagged { Ok(Self::AddFileIndicators) }
|
||||
else { Ok(Self::JustFilenames) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ShowIcons {
|
||||
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
|
||||
if matches.has(&flags::NO_ICONS)? || !matches.has(&flags::ICONS)? {
|
||||
Ok(Self::Off)
|
||||
}
|
||||
else if let Some(columns) = vars.get(vars::EXA_ICON_SPACING).and_then(|s| s.into_string().ok()) {
|
||||
match columns.parse() {
|
||||
Ok(width) => {
|
||||
Ok(Self::On(width))
|
||||
}
|
||||
Err(e) => {
|
||||
let source = NumberSource::Env(vars::EXA_ICON_SPACING);
|
||||
Err(OptionsError::FailedParse(columns, source, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Ok(Self::On(1))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +1,24 @@
|
|||
//! Parsing the options for `FileFilter`.
|
||||
|
||||
use fs::{DotFilter, PlatformMetadata};
|
||||
use fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore};
|
||||
use crate::fs::DotFilter;
|
||||
use crate::fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore};
|
||||
|
||||
use options::{flags, Misfire};
|
||||
use options::parser::MatchedFlags;
|
||||
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,61 +29,81 @@ 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
|
||||
}
|
||||
#[cfg(unix)]
|
||||
"inode" => {
|
||||
Self::FileInode
|
||||
}
|
||||
"type" => {
|
||||
Self::FileType
|
||||
}
|
||||
"none" => {
|
||||
Self::Unsorted
|
||||
}
|
||||
_ => {
|
||||
return Err(OptionsError::BadArgument(&flags::SORT, word.into()));
|
||||
}
|
||||
};
|
||||
|
||||
match SortField::to_platform_metadata(field) {
|
||||
Some(m) => match m.check_supported() {
|
||||
Ok(_) => Ok(field),
|
||||
Err(misfire) => Err(misfire),
|
||||
},
|
||||
None => Ok(field),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_platform_metadata(field: Self) -> Option<PlatformMetadata> {
|
||||
match field {
|
||||
SortField::ModifiedDate => Some(PlatformMetadata::ModifiedTime),
|
||||
SortField::ChangedDate => Some(PlatformMetadata::ChangedTime),
|
||||
SortField::AccessedDate => Some(PlatformMetadata::AccessedTime),
|
||||
SortField::CreatedDate => Some(PlatformMetadata::CreatedTime),
|
||||
_ => None
|
||||
}
|
||||
Ok(field)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,10 +139,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,23 +154,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -160,54 +179,57 @@ impl DotFilter {
|
|||
impl IgnorePatterns {
|
||||
|
||||
/// Determines the set of glob patterns to use based on the
|
||||
/// `--ignore-patterns` argument’s value. This is a list of strings
|
||||
/// `--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::*;
|
||||
use std::ffi::OsString;
|
||||
use options::flags;
|
||||
use options::parser::Flag;
|
||||
use crate::options::flags;
|
||||
use crate::options::parser::Flag;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
use options::parser::Arg;
|
||||
use options::test::parse_for_test;
|
||||
use options::test::Strictnesses::*;
|
||||
use crate::options::parser::Arg;
|
||||
use crate::options::test::parse_for_test;
|
||||
use crate::options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::TREE, &flags::IGNORE_GLOB, &flags::GIT_IGNORE ];
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
|
@ -239,13 +261,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"))));
|
||||
}
|
||||
|
||||
|
||||
|
@ -261,35 +283,34 @@ 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));
|
||||
}
|
||||
|
||||
|
||||
mod ignore_patterns {
|
||||
use super::*;
|
||||
use std::iter::FromIterator;
|
||||
use glob;
|
||||
|
||||
fn pat(string: &'static str) -> glob::Pattern {
|
||||
glob::Pattern::new(string).unwrap()
|
||||
}
|
||||
|
||||
// Various numbers of globs
|
||||
test!(none: IgnorePatterns <- []; Both => Ok(IgnorePatterns::empty()));
|
||||
test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ])));
|
||||
test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ])));
|
||||
test!(loads: IgnorePatterns <- ["-I*|?|.|*"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ])));
|
||||
test!(none: IgnorePatterns <- []; Both => Ok(IgnorePatterns::empty()));
|
||||
test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ])));
|
||||
test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ])));
|
||||
test!(loads: IgnorePatterns <- ["-I*|?|.|*"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ])));
|
||||
|
||||
// Overriding
|
||||
test!(overridden: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.mp3") ])));
|
||||
test!(overridden_2: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.MP3") ])));
|
||||
test!(overridden_3: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
|
||||
test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(Misfire::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
|
||||
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(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 options::parser::{Arg, Args, Values, TakesValue};
|
||||
use crate::options::parser::{Arg, Args, TakesValue, Values};
|
||||
|
||||
|
||||
// exa options
|
||||
|
@ -32,13 +32,14 @@ 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 };
|
||||
pub static BYTES: Arg = Arg { short: Some(b'B'), long: "bytes", takes_value: TakesValue::Forbidden };
|
||||
pub static GROUP: Arg = Arg { short: Some(b'g'), long: "group", takes_value: TakesValue::Forbidden };
|
||||
pub static NUMERIC: Arg = Arg { short: Some(b'n'), long: "numeric", takes_value: TakesValue::Forbidden };
|
||||
pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_value: TakesValue::Forbidden };
|
||||
pub static ICONS: Arg = Arg { short: None, long: "icons", takes_value: TakesValue::Forbidden };
|
||||
pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden };
|
||||
|
@ -53,9 +54,17 @@ pub static TIME_STYLE: Arg = Arg { short: None, long: "time-style", takes_
|
|||
const TIMES: Values = &["modified", "changed", "accessed", "created"];
|
||||
const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso"];
|
||||
|
||||
// suppressing columns
|
||||
pub static NO_PERMISSIONS: Arg = Arg { short: None, long: "no-permissions", takes_value: TakesValue::Forbidden };
|
||||
pub static NO_FILESIZE: Arg = Arg { short: None, long: "no-filesize", takes_value: TakesValue::Forbidden };
|
||||
pub static NO_USER: Arg = Arg { short: None, long: "no-user", takes_value: TakesValue::Forbidden };
|
||||
pub static NO_TIME: Arg = Arg { short: None, long: "no-time", takes_value: TakesValue::Forbidden };
|
||||
pub static NO_ICONS: Arg = Arg { short: None, long: "no-icons", takes_value: TakesValue::Forbidden };
|
||||
|
||||
// optional feature options
|
||||
pub static GIT: Arg = Arg { short: None, long: "git", takes_value: TakesValue::Forbidden };
|
||||
pub static EXTENDED: Arg = Arg { short: Some(b'@'), long: "extended", takes_value: TakesValue::Forbidden };
|
||||
pub static GIT: Arg = Arg { short: None, long: "git", takes_value: TakesValue::Forbidden };
|
||||
pub static EXTENDED: Arg = Arg { short: Some(b'@'), long: "extended", takes_value: TakesValue::Forbidden };
|
||||
pub static OCTAL: Arg = Arg { short: None, long: "octal-permissions", takes_value: TakesValue::Forbidden };
|
||||
|
||||
|
||||
pub static ALL_ARGS: Args = Args(&[
|
||||
|
@ -67,8 +76,9 @@ pub static ALL_ARGS: Args = Args(&[
|
|||
&ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST,
|
||||
&IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS,
|
||||
|
||||
&BINARY, &BYTES, &GROUP, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED,
|
||||
&BINARY, &BYTES, &GROUP, &NUMERIC, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED,
|
||||
&BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE,
|
||||
&NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &NO_ICONS,
|
||||
|
||||
&GIT, &EXTENDED,
|
||||
&GIT, &EXTENDED, &OCTAL
|
||||
]);
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
use std::fmt;
|
||||
|
||||
use options::flags;
|
||||
use options::parser::MatchedFlags;
|
||||
use fs::feature::xattr;
|
||||
use crate::fs::feature::xattr;
|
||||
use crate::options::flags;
|
||||
use crate::options::parser::MatchedFlags;
|
||||
|
||||
|
||||
static OPTIONS: &str = r##"
|
||||
static USAGE_PART1: &str = "Usage:
|
||||
exa [options] [files...]
|
||||
|
||||
META OPTIONS
|
||||
-?, --help show list of command-line options
|
||||
-v, --version show version of exa
|
||||
|
||||
|
@ -19,56 +22,55 @@ DISPLAY OPTIONS
|
|||
-F, --classify display type indicator by file names
|
||||
--colo[u]r=WHEN when to use terminal colours (always, auto, never)
|
||||
--colo[u]r-scale highlight levels of file sizes distinctly
|
||||
--icons display icons
|
||||
--no-icons don't display icons (always overrides --icons)
|
||||
|
||||
FILTERING AND SORTING OPTIONS
|
||||
-a, --all show hidden and 'dot' files
|
||||
-d, --list-dirs list directories like regular files
|
||||
-d, --list-dirs list directories as files; don't list their contents
|
||||
-L, --level DEPTH limit the depth of recursion
|
||||
-r, --reverse reverse the sort order
|
||||
-s, --sort SORT_FIELD which field to sort by
|
||||
--group-directories-first list directories before other files
|
||||
-D, --only-dirs list only directories
|
||||
-I, --ignore-glob GLOBS glob patterns (pipe-separated) of files to ignore
|
||||
--git-ignore Ignore files mentioned in '.gitignore'
|
||||
-I, --ignore-glob GLOBS glob patterns (pipe-separated) of files to ignore";
|
||||
|
||||
static USAGE_PART2: &str = " \
|
||||
Valid sort fields: name, Name, extension, Extension, size, type,
|
||||
modified, accessed, created, inode, and none.
|
||||
date, time, old, and new all refer to modified.
|
||||
"##;
|
||||
|
||||
static LONG_OPTIONS: &str = r##"
|
||||
LONG VIEW OPTIONS
|
||||
-b, --binary list file sizes with binary prefixes
|
||||
-B, --bytes list file sizes in bytes, without any prefixes
|
||||
-g, --group list each file's group
|
||||
-h, --header add a header row to each column
|
||||
-H, --links list each file's number of hard links
|
||||
-i, --inode list each file's inode number
|
||||
-m, --modified use the modified timestamp field
|
||||
-S, --blocks show number of file system blocks
|
||||
-t, --time FIELD which timestamp field to list (modified, accessed, created)
|
||||
-u, --accessed use the accessed timestamp field
|
||||
-U, --created use the created timestamp field
|
||||
--time-style how to format timestamps (default, iso, long-iso, full-iso)"##;
|
||||
-b, --binary list file sizes with binary prefixes
|
||||
-B, --bytes list file sizes in bytes, without any prefixes
|
||||
-g, --group list each file's group
|
||||
-h, --header add a header row to each column
|
||||
-H, --links list each file's number of hard links
|
||||
-i, --inode list each file's inode number
|
||||
-m, --modified use the modified timestamp field
|
||||
-n, --numeric list numeric user and group IDs
|
||||
-S, --blocks show number of file system blocks
|
||||
-t, --time FIELD which timestamp field to list (modified, accessed, created)
|
||||
-u, --accessed use the accessed timestamp field
|
||||
-U, --created use the created timestamp field
|
||||
--changed use the changed timestamp field
|
||||
--time-style how to format timestamps (default, iso, long-iso, full-iso)
|
||||
--no-permissions suppress the permissions field
|
||||
--octal-permissions list each file's permission in octal format
|
||||
--no-filesize suppress the filesize field
|
||||
--no-user suppress the user field
|
||||
--no-time suppress the time field";
|
||||
|
||||
static GIT_HELP: &str = r##" --git list each file's Git status, if tracked or ignored"##;
|
||||
static EXTENDED_HELP: &str = r##" -@, --extended list each file's extended attributes and sizes"##;
|
||||
static GIT_FILTER_HELP: &str = " --git-ignore ignore files mentioned in '.gitignore'";
|
||||
static GIT_VIEW_HELP: &str = " --git list each file's Git status, if tracked or ignored";
|
||||
static EXTENDED_HELP: &str = " -@, --extended list each file's extended attributes and sizes";
|
||||
|
||||
|
||||
/// All the information needed to display the help text, which depends
|
||||
/// on which features are enabled and whether the user only wants to
|
||||
/// see one section’s help.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct HelpString {
|
||||
|
||||
/// Only show the help for the long section, not all the help.
|
||||
only_long: bool,
|
||||
|
||||
/// Whether the --git option should be included in the help.
|
||||
git: bool,
|
||||
|
||||
/// Whether the --extended option should be included in the help.
|
||||
xattrs: bool,
|
||||
}
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct HelpString;
|
||||
|
||||
impl HelpString {
|
||||
|
||||
|
@ -79,15 +81,12 @@ 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)
|
||||
}
|
||||
else {
|
||||
Ok(()) // no help needs to be shown
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,58 +95,51 @@ impl fmt::Display for HelpString {
|
|||
|
||||
/// Format this help options into an actual string of help
|
||||
/// text to be displayed to the user.
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
writeln!(f, "Usage:\n exa [options] [files...]")?;
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(f, "{}", USAGE_PART1)?;
|
||||
|
||||
if !self.only_long {
|
||||
write!(f, "{}", OPTIONS)?;
|
||||
if cfg!(feature = "git") {
|
||||
write!(f, "\n{}", GIT_FILTER_HELP)?;
|
||||
}
|
||||
|
||||
write!(f, "{}", LONG_OPTIONS)?;
|
||||
write!(f, "\n{}", USAGE_PART2)?;
|
||||
|
||||
if self.git {
|
||||
write!(f, "\n{}", GIT_HELP)?;
|
||||
if cfg!(feature = "git") {
|
||||
write!(f, "\n{}", GIT_VIEW_HELP)?;
|
||||
}
|
||||
|
||||
if self.xattrs {
|
||||
if xattr::ENABLED {
|
||||
write!(f, "\n{}", EXTENDED_HELP)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
writeln!(f)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use options::Options;
|
||||
use std::ffi::OsString;
|
||||
|
||||
fn os(input: &'static str) -> OsString {
|
||||
let mut os = OsString::new();
|
||||
os.push(input);
|
||||
os
|
||||
}
|
||||
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,143 +0,0 @@
|
|||
use std::ffi::OsString;
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use glob;
|
||||
|
||||
use options::{flags, HelpString, VersionString};
|
||||
use 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 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,34 @@
|
|||
//! it’s clear what the user wants.
|
||||
|
||||
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use fs::dir_action::DirAction;
|
||||
use fs::filter::FileFilter;
|
||||
use output::{View, Mode, details, grid_details};
|
||||
use crate::fs::dir_action::DirAction;
|
||||
use crate::fs::filter::{FileFilter, GitIgnore};
|
||||
use crate::output::{View, Mode, details, grid_details};
|
||||
use crate::theme::Options as ThemeOptions;
|
||||
|
||||
mod style;
|
||||
mod dir_action;
|
||||
mod file_name;
|
||||
mod filter;
|
||||
mod flags;
|
||||
mod theme;
|
||||
mod view;
|
||||
|
||||
mod error;
|
||||
pub use self::error::{OptionsError, NumberSource};
|
||||
|
||||
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
|
||||
|
@ -109,8 +111,14 @@ pub struct Options {
|
|||
/// How to sort and filter files before outputting them.
|
||||
pub filter: FileFilter,
|
||||
|
||||
/// The type of output to use (lines, grid, or details).
|
||||
/// The user’s preference of view to use (lines, grid, details, or
|
||||
/// grid-details) along with the options on how to render file names.
|
||||
/// If the view requires the terminal to have a width, and there is no
|
||||
/// width, then the view will be downgraded.
|
||||
pub view: View,
|
||||
|
||||
/// The options to make up the styles of the UI and file names.
|
||||
pub theme: ThemeOptions,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
|
@ -119,60 +127,96 @@ 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 {
|
||||
use options::parser::{Matches, Strictness};
|
||||
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) {
|
||||
None => Strictness::UseLastArguments,
|
||||
Some(ref t) if t.is_empty() => Strictness::UseLastArguments,
|
||||
_ => Strictness::ComplainAboutRedundantArguments,
|
||||
Some(_) => Strictness::ComplainAboutRedundantArguments,
|
||||
};
|
||||
|
||||
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
|
||||
/// status column. It’s only worth trying to discover a repository if the
|
||||
/// results will end up being displayed.
|
||||
pub fn should_scan_for_git(&self) -> bool {
|
||||
if self.filter.git_ignore == GitIgnore::CheckAndIgnore {
|
||||
return true;
|
||||
}
|
||||
|
||||
match self.view.mode {
|
||||
Mode::Details(details::Options { table: Some(ref table), .. }) |
|
||||
Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.extra_columns.git,
|
||||
Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.columns.git,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let dir_action = DirAction::deduce(matches)?;
|
||||
let filter = FileFilter::deduce(matches)?;
|
||||
let view = View::deduce(matches, vars)?;
|
||||
fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
|
||||
if cfg!(not(feature = "git")) &&
|
||||
matches.has_where_any(|f| f.matches(&flags::GIT) || f.matches(&flags::GIT_IGNORE)).is_some() {
|
||||
return Err(OptionsError::Unsupported(String::from(
|
||||
"Options --git and --git-ignore can't be used because `git` feature was disabled in this build of exa"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Options { dir_action, view, filter })
|
||||
let view = View::deduce(matches, vars)?;
|
||||
let dir_action = DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)))?;
|
||||
let filter = FileFilter::deduce(matches)?;
|
||||
let theme = ThemeOptions::deduce(matches, vars)?;
|
||||
|
||||
Ok(Self { dir_action, filter, view, theme })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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 options::parser::{Arg, MatchedFlags};
|
||||
use std::ffi::OsString;
|
||||
use crate::options::parser::{Arg, MatchedFlags};
|
||||
use std::ffi::OsStr;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum Strictnesses {
|
||||
Last,
|
||||
Complain,
|
||||
|
@ -184,62 +228,26 @@ pub mod test {
|
|||
/// both, then both should resolve to the same result.
|
||||
///
|
||||
/// It returns a vector with one or two elements in.
|
||||
/// These elements can then be tested with assert_eq or what have you.
|
||||
/// These elements can then be tested with `assert_eq` or what have you.
|
||||
pub fn parse_for_test<T, F>(inputs: &[&str], args: &'static [&'static Arg], strictnesses: Strictnesses, get: F) -> Vec<T>
|
||||
where F: Fn(&MatchedFlags) -> T
|
||||
where F: Fn(&MatchedFlags<'_>) -> T
|
||||
{
|
||||
use self::Strictnesses::*;
|
||||
use options::parser::{Args, Strictness};
|
||||
use crate::options::parser::{Args, Strictness};
|
||||
|
||||
let bits = inputs.into_iter().map(|&o| os(o)).collect::<Vec<OsString>>();
|
||||
let bits = inputs.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 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, Eq, Debug, Copy, Clone)]
|
||||
pub enum Flag {
|
||||
Short(ShortArg),
|
||||
Long(LongArg),
|
||||
|
@ -60,24 +60,24 @@ 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),
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
Self::Short(short) => write!(f, "-{}", *short as char),
|
||||
Self::Long(long) => write!(f, "--{}", long),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether redundant arguments should be considered a problem.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum Strictness {
|
||||
|
||||
/// Throw an error when an argument doesn’t do anything, either because
|
||||
|
@ -91,7 +91,7 @@ pub enum Strictness {
|
|||
|
||||
/// Whether a flag takes a value. This is applicable to both long and short
|
||||
/// arguments.
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum TakesValue {
|
||||
|
||||
/// This flag has to be followed by a value.
|
||||
|
@ -101,11 +101,14 @@ pub enum TakesValue {
|
|||
|
||||
/// This flag will throw an error if there’s a value after it.
|
||||
Forbidden,
|
||||
|
||||
/// This flag may be followed by a value to override its defaults
|
||||
Optional(Option<Values>),
|
||||
}
|
||||
|
||||
|
||||
/// An **argument** can be matched by one of the user’s input strings.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct Arg {
|
||||
|
||||
/// The short argument that matches it, if any.
|
||||
|
@ -120,7 +123,7 @@ pub struct Arg {
|
|||
}
|
||||
|
||||
impl fmt::Display for Arg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(f, "--{}", self.long)?;
|
||||
|
||||
if let Some(short) = self.short {
|
||||
|
@ -133,7 +136,7 @@ impl fmt::Display for Arg {
|
|||
|
||||
|
||||
/// Literally just several args.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Args(pub &'static [&'static Arg]);
|
||||
|
||||
impl Args {
|
||||
|
@ -141,10 +144,8 @@ impl Args {
|
|||
/// Iterates over the given list of command-line arguments and parses
|
||||
/// them into a list of matched flags and free strings.
|
||||
pub fn parse<'args, I>(&self, inputs: I, strictness: Strictness) -> Result<Matches<'args>, ParseError>
|
||||
where I: IntoIterator<Item=&'args OsString> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use self::TakesValue::*;
|
||||
|
||||
where I: IntoIterator<Item = &'args OsStr>
|
||||
{
|
||||
let mut parsing = true;
|
||||
|
||||
// The results that get built up.
|
||||
|
@ -156,13 +157,13 @@ impl Args {
|
|||
// doesn’t have one in its string so it needs the next one.
|
||||
let mut inputs = inputs.into_iter();
|
||||
while let Some(arg) = inputs.next() {
|
||||
let bytes = arg.as_bytes();
|
||||
let bytes = os_str_to_bytes(arg);
|
||||
|
||||
// Stop parsing if one of the arguments is the literal string “--”.
|
||||
// This allows a file named “--arg” to be specified by passing in
|
||||
// the pair “-- --arg”, without it getting matched as a flag that
|
||||
// doesn’t exist.
|
||||
if !parsing {
|
||||
if ! parsing {
|
||||
frees.push(arg)
|
||||
}
|
||||
else if arg == "--" {
|
||||
|
@ -171,7 +172,7 @@ impl Args {
|
|||
|
||||
// If the string starts with *two* dashes then it’s a long argument.
|
||||
else if bytes.starts_with(b"--") {
|
||||
let long_arg_name = OsStr::from_bytes(&bytes[2..]);
|
||||
let long_arg_name = bytes_to_os_str(&bytes[2..]);
|
||||
|
||||
// If there’s an equals in it, then the string before the
|
||||
// equals will be the flag’s name, and the string after it
|
||||
|
@ -180,8 +181,9 @@ impl Args {
|
|||
let arg = self.lookup_long(before)?;
|
||||
let flag = Flag::Long(arg.long);
|
||||
match arg.takes_value {
|
||||
Necessary(_) => result_flags.push((flag, Some(after))),
|
||||
Forbidden => return Err(ParseError::ForbiddenValue { flag })
|
||||
TakesValue::Necessary(_) |
|
||||
TakesValue::Optional(_) => result_flags.push((flag, Some(after))),
|
||||
TakesValue::Forbidden => return Err(ParseError::ForbiddenValue { flag }),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,8 +193,10 @@ 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)));
|
||||
}
|
||||
|
@ -200,6 +204,14 @@ impl Args {
|
|||
return Err(ParseError::NeedsValue { flag, values })
|
||||
}
|
||||
}
|
||||
TakesValue::Optional(_) => {
|
||||
if let Some(next_arg) = inputs.next() {
|
||||
result_flags.push((flag, Some(next_arg)));
|
||||
}
|
||||
else {
|
||||
result_flags.push((flag, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +219,7 @@ impl Args {
|
|||
// If the string starts with *one* dash then it’s one or more
|
||||
// short arguments.
|
||||
else if bytes.starts_with(b"-") && arg != "-" {
|
||||
let short_arg = OsStr::from_bytes(&bytes[1..]);
|
||||
let short_arg = bytes_to_os_str(&bytes[1..]);
|
||||
|
||||
// If there’s an equals in it, then the argument immediately
|
||||
// before the equals was the one that has the value, with the
|
||||
|
@ -222,15 +234,20 @@ impl Args {
|
|||
// it’s an error if any of the first set of arguments actually
|
||||
// takes a value.
|
||||
if let Some((before, after)) = split_on_equals(short_arg) {
|
||||
let (arg_with_value, other_args) = before.as_bytes().split_last().unwrap();
|
||||
let (arg_with_value, other_args) = os_str_to_bytes(before).split_last().unwrap();
|
||||
|
||||
// Process the characters immediately following the dash...
|
||||
for byte in other_args {
|
||||
let arg = self.lookup_short(*byte)?;
|
||||
let flag = Flag::Short(*byte);
|
||||
match arg.takes_value {
|
||||
Forbidden => result_flags.push((flag, None)),
|
||||
Necessary(values) => return Err(ParseError::NeedsValue { flag, values })
|
||||
TakesValue::Forbidden |
|
||||
TakesValue::Optional(_) => {
|
||||
result_flags.push((flag, None));
|
||||
}
|
||||
TakesValue::Necessary(values) => {
|
||||
return Err(ParseError::NeedsValue { flag, values });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,8 +255,13 @@ impl Args {
|
|||
let arg = self.lookup_short(*arg_with_value)?;
|
||||
let flag = Flag::Short(arg.short.unwrap());
|
||||
match arg.takes_value {
|
||||
Necessary(_) => result_flags.push((flag, Some(after))),
|
||||
Forbidden => return Err(ParseError::ForbiddenValue { flag })
|
||||
TakesValue::Necessary(_) |
|
||||
TakesValue::Optional(_) => {
|
||||
result_flags.push((flag, Some(after)));
|
||||
}
|
||||
TakesValue::Forbidden => {
|
||||
return Err(ParseError::ForbiddenValue { flag });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -256,22 +278,35 @@ impl Args {
|
|||
// -abx => error
|
||||
//
|
||||
else {
|
||||
for (index, byte) in bytes.into_iter().enumerate().skip(1) {
|
||||
for (index, byte) in bytes.iter().enumerate().skip(1) {
|
||||
let arg = self.lookup_short(*byte)?;
|
||||
let flag = Flag::Short(*byte);
|
||||
match arg.takes_value {
|
||||
Forbidden => result_flags.push((flag, None)),
|
||||
Necessary(values) => {
|
||||
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))));
|
||||
result_flags.push((flag, Some(bytes_to_os_str(remnants))));
|
||||
break;
|
||||
}
|
||||
else if let Some(next_arg) = inputs.next() {
|
||||
result_flags.push((flag, Some(next_arg)));
|
||||
}
|
||||
else {
|
||||
return Err(ParseError::NeedsValue { flag, values })
|
||||
match arg.takes_value {
|
||||
TakesValue::Forbidden => {
|
||||
unreachable!()
|
||||
}
|
||||
TakesValue::Necessary(_) => {
|
||||
return Err(ParseError::NeedsValue { flag, values });
|
||||
}
|
||||
TakesValue::Optional(_) => {
|
||||
result_flags.push((flag, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -289,14 +324,14 @@ impl Args {
|
|||
}
|
||||
|
||||
fn lookup_short(&self, short: ShortArg) -> Result<&Arg, ParseError> {
|
||||
match self.0.into_iter().find(|arg| arg.short == Some(short)) {
|
||||
match self.0.iter().find(|arg| arg.short == Some(short)) {
|
||||
Some(arg) => Ok(arg),
|
||||
None => Err(ParseError::UnknownShortArgument { attempt: short })
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_long<'b>(&self, long: &'b OsStr) -> Result<&Arg, ParseError> {
|
||||
match self.0.into_iter().find(|arg| arg.long == long) {
|
||||
match self.0.iter().find(|arg| arg.long == long) {
|
||||
Some(arg) => Ok(arg),
|
||||
None => Err(ParseError::UnknownArgument { attempt: long.to_os_string() })
|
||||
}
|
||||
|
@ -305,18 +340,18 @@ impl Args {
|
|||
|
||||
|
||||
/// The **matches** are the result of parsing the user’s command-line strings.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Matches<'args> {
|
||||
|
||||
/// The flags that were parsed from the user’s input.
|
||||
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>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct MatchedFlags<'args> {
|
||||
|
||||
/// The individual flags from the user’s input, in the order they were
|
||||
|
@ -336,8 +371,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
|
||||
|
@ -345,7 +381,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()
|
||||
|
@ -353,16 +389,24 @@ 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()
|
||||
.find(|tuple| tuple.1.is_none() && predicate(&tuple.0))
|
||||
.map(|tuple| &tuple.0);
|
||||
Ok(any)
|
||||
Ok(self.has_where_any(predicate))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the first found argument that satisfies the predicate, or
|
||||
/// nothing if none is found, with strict mode having no effect.
|
||||
///
|
||||
/// You’ll have to test the resulting flag to see which argument it was.
|
||||
pub fn has_where_any<P>(&self, predicate: P) -> Option<&Flag>
|
||||
where P: Fn(&Flag) -> bool {
|
||||
self.flags.iter().rev()
|
||||
.find(|tuple| tuple.1.is_none() && predicate(&tuple.0))
|
||||
.map(|tuple| &tuple.0)
|
||||
}
|
||||
|
||||
// This code could probably be better.
|
||||
// Both ‘has’ and ‘get’ immediately begin with a conditional, which makes
|
||||
// me think the functionality could be moved to inside Strictness.
|
||||
|
@ -370,7 +414,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))
|
||||
}
|
||||
|
||||
|
@ -379,15 +423,15 @@ 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()
|
||||
.filter(|tuple| tuple.1.is_some() && predicate(&tuple.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if those.len() < 2 { Ok(those.first().cloned().map(|t| t.1.unwrap())) }
|
||||
else { Err(Misfire::Duplicate(those[0].0.clone(), those[1].0.clone())) }
|
||||
if those.len() < 2 { Ok(those.first().copied().map(|t| t.1.unwrap())) }
|
||||
else { Err(OptionsError::Duplicate(those[0].0, those[1].0)) }
|
||||
}
|
||||
else {
|
||||
let found = self.flags.iter().rev()
|
||||
|
@ -418,7 +462,7 @@ impl<'a> MatchedFlags<'a> {
|
|||
|
||||
/// A problem with the user’s input that meant it couldn’t be parsed into a
|
||||
/// coherent list of arguments.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum ParseError {
|
||||
|
||||
/// A flag that has to take a value was not given one.
|
||||
|
@ -437,24 +481,54 @@ 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn os_str_to_bytes<'b>(s: &'b OsStr) -> &'b [u8]{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
return s.as_bytes()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn bytes_to_os_str<'b>(b: &'b [u8]) -> &'b OsStr{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
return OsStr::from_bytes(b);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn os_str_to_bytes<'b>(s: &'b OsStr) -> &'b [u8]{
|
||||
return s.to_str().unwrap().as_bytes()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn bytes_to_os_str<'b>(b: &'b [u8]) -> &'b OsStr{
|
||||
use std::str;
|
||||
|
||||
return OsStr::new(str::from_utf8(b).unwrap());
|
||||
}
|
||||
|
||||
/// Splits a string on its `=` character, returning the two substrings on
|
||||
/// either side. Returns `None` if there’s no equals or a string is missing.
|
||||
fn split_on_equals(input: &OsStr) -> Option<(&OsStr, &OsStr)> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
if let Some(index) = input.as_bytes().iter().position(|elem| *elem == b'=') {
|
||||
let (before, after) = input.as_bytes().split_at(index);
|
||||
if let Some(index) = os_str_to_bytes(input).iter().position(|elem| *elem == b'=') {
|
||||
let (before, after) = os_str_to_bytes(input).split_at(index);
|
||||
|
||||
// The after string contains the = that we need to remove.
|
||||
if !before.is_empty() && after.len() >= 2 {
|
||||
return Some((OsStr::from_bytes(before),
|
||||
OsStr::from_bytes(&after[1..])))
|
||||
if ! before.is_empty() && after.len() >= 2 {
|
||||
return Some((bytes_to_os_str(before),
|
||||
bytes_to_os_str(&after[1..])))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -462,24 +536,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);
|
||||
}
|
||||
};
|
||||
|
@ -487,8 +553,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))));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -510,30 +576,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);
|
||||
}
|
||||
};
|
||||
|
@ -543,10 +603,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));
|
||||
}
|
||||
};
|
||||
|
@ -614,8 +674,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' });
|
||||
|
@ -655,7 +715,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)) ],
|
||||
|
@ -667,8 +727,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)),
|
||||
|
|
|
@ -1,520 +0,0 @@
|
|||
use ansi_term::Style;
|
||||
use glob;
|
||||
|
||||
use fs::File;
|
||||
use options::{flags, Vars, Misfire};
|
||||
use options::parser::MatchedFlags;
|
||||
use output::file_name::{FileStyle, Classify};
|
||||
use style::Colours;
|
||||
|
||||
|
||||
/// Under what circumstances we should display coloured, rather than plain,
|
||||
/// output to the terminal.
|
||||
///
|
||||
/// By default, we want to display the colours when stdout can display them.
|
||||
/// Turning them on when output is going to, say, a pipe, would make programs
|
||||
/// such as `grep` or `more` not work properly. So the `Automatic` mode does
|
||||
/// this check and only displays colours when they can be truly appreciated.
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum TerminalColours {
|
||||
|
||||
/// Display them even when output isn’t going to a terminal.
|
||||
Always,
|
||||
|
||||
/// Display them when output is going to a terminal, but not otherwise.
|
||||
Automatic,
|
||||
|
||||
/// Never display them, even when output is going to a terminal.
|
||||
Never,
|
||||
}
|
||||
|
||||
impl Default for TerminalColours {
|
||||
fn default() -> TerminalColours {
|
||||
TerminalColours::Automatic
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TerminalColours {
|
||||
|
||||
/// Determine which terminal colour conditions to use.
|
||||
fn deduce(matches: &MatchedFlags) -> Result<TerminalColours, Misfire> {
|
||||
|
||||
let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? {
|
||||
Some(w) => w,
|
||||
None => return Ok(TerminalColours::default()),
|
||||
};
|
||||
|
||||
if word == "always" {
|
||||
Ok(TerminalColours::Always)
|
||||
}
|
||||
else if word == "auto" || word == "automatic" {
|
||||
Ok(TerminalColours::Automatic)
|
||||
}
|
||||
else if word == "never" {
|
||||
Ok(TerminalColours::Never)
|
||||
}
|
||||
else {
|
||||
Err(Misfire::BadArgument(&flags::COLOR, word.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// **Styles**, which is already an overloaded term, is a pair of view option
|
||||
/// sets that happen to both be affected by `LS_COLORS` and `EXA_COLORS`.
|
||||
/// Because it’s better to only iterate through that once, the two are deduced
|
||||
/// together.
|
||||
pub struct Styles {
|
||||
|
||||
/// The colours to paint user interface elements, like the date column,
|
||||
/// and file kinds, such as directories.
|
||||
pub colours: Colours,
|
||||
|
||||
/// The colours to paint the names of files that match glob patterns
|
||||
/// (and the classify option).
|
||||
pub style: FileStyle,
|
||||
}
|
||||
|
||||
impl Styles {
|
||||
|
||||
#[allow(trivial_casts)] // the "as Box<_>" stuff below warns about this for some reason
|
||||
pub fn deduce<V, TW>(matches: &MatchedFlags, vars: &V, widther: TW) -> Result<Self, Misfire>
|
||||
where TW: Fn() -> Option<usize>, V: Vars {
|
||||
use self::TerminalColours::*;
|
||||
use info::filetype::FileExtensions;
|
||||
use output::file_name::NoFileColours;
|
||||
|
||||
let classify = Classify::deduce(matches)?;
|
||||
|
||||
// Before we do anything else, figure out if we need to consider
|
||||
// custom colours at all
|
||||
let tc = TerminalColours::deduce(matches)?;
|
||||
if tc == Never || (tc == Automatic && widther().is_none()) {
|
||||
return Ok(Styles {
|
||||
colours: Colours::plain(),
|
||||
style: FileStyle { classify, exts: Box::new(NoFileColours) },
|
||||
});
|
||||
}
|
||||
|
||||
// Parse the environment variables into colours and extension mappings
|
||||
let scale = matches.has_where(|f| f.matches(&flags::COLOR_SCALE) || f.matches(&flags::COLOUR_SCALE))?;
|
||||
let mut colours = Colours::colourful(scale.is_some());
|
||||
|
||||
let (exts, use_default_filetypes) = parse_color_vars(vars, &mut colours);
|
||||
|
||||
// Use between 0 and 2 file name highlighters
|
||||
let exts = match (exts.is_non_empty(), use_default_filetypes) {
|
||||
(false, false) => Box::new(NoFileColours) as Box<_>,
|
||||
(false, true) => Box::new(FileExtensions) as Box<_>,
|
||||
( true, false) => Box::new(exts) as Box<_>,
|
||||
( true, true) => Box::new((exts, FileExtensions)) as Box<_>,
|
||||
};
|
||||
|
||||
let style = FileStyle { classify, exts };
|
||||
Ok(Styles { colours, style })
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the environment variables into LS_COLORS pairs, putting file glob
|
||||
/// colours into the `ExtensionMappings` that gets returned, and using the
|
||||
/// two-character UI codes to modify the mutable `Colours`.
|
||||
///
|
||||
/// Also returns if the EXA_COLORS variable should reset the existing file
|
||||
/// type mappings or not. The `reset` code needs to be the first one.
|
||||
fn parse_color_vars<V: Vars>(vars: &V, colours: &mut Colours) -> (ExtensionMappings, bool) {
|
||||
use options::vars;
|
||||
use style::LSColors;
|
||||
|
||||
let mut exts = ExtensionMappings::default();
|
||||
|
||||
if let Some(lsc) = vars.get(vars::LS_COLORS) {
|
||||
let lsc = lsc.to_string_lossy();
|
||||
LSColors(lsc.as_ref()).each_pair(|pair| {
|
||||
if !colours.set_ls(&pair) {
|
||||
match glob::Pattern::new(pair.key) {
|
||||
Ok(pat) => exts.add(pat, pair.to_style()),
|
||||
Err(e) => warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut use_default_filetypes = true;
|
||||
|
||||
if let Some(exa) = vars.get(vars::EXA_COLORS) {
|
||||
let exa = exa.to_string_lossy();
|
||||
|
||||
// Is this hacky? Yes.
|
||||
if exa == "reset" || exa.starts_with("reset:") {
|
||||
use_default_filetypes = false;
|
||||
}
|
||||
|
||||
LSColors(exa.as_ref()).each_pair(|pair| {
|
||||
if !colours.set_ls(&pair) && !colours.set_exa(&pair) {
|
||||
match glob::Pattern::new(pair.key) {
|
||||
Ok(pat) => exts.add(pat, pair.to_style()),
|
||||
Err(e) => warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e),
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
(exts, use_default_filetypes)
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Default)]
|
||||
struct ExtensionMappings {
|
||||
mappings: Vec<(glob::Pattern, Style)>
|
||||
}
|
||||
|
||||
// Loop through backwards so that colours specified later in the list override
|
||||
// colours specified earlier, like we do with options and strict mode
|
||||
|
||||
use output::file_name::FileColours;
|
||||
impl FileColours for ExtensionMappings {
|
||||
fn colour_file(&self, file: &File) -> Option<Style> {
|
||||
self.mappings
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|t| t.0.matches(&file.name))
|
||||
.map (|t| t.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionMappings {
|
||||
fn is_non_empty(&self) -> bool {
|
||||
!self.mappings.is_empty()
|
||||
}
|
||||
|
||||
fn add(&mut self, pattern: glob::Pattern, style: Style) {
|
||||
self.mappings.push((pattern, style))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Classify {
|
||||
fn deduce(matches: &MatchedFlags) -> Result<Classify, Misfire> {
|
||||
let flagged = matches.has(&flags::CLASSIFY)?;
|
||||
|
||||
Ok(if flagged { Classify::AddFileIndicators }
|
||||
else { Classify::JustFilenames })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod terminal_test {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
use options::flags;
|
||||
use options::parser::{Flag, Arg};
|
||||
|
||||
use options::test::parse_for_test;
|
||||
use options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR ];
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $inputs:expr; $stricts:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| TerminalColours::deduce(mf)) {
|
||||
assert_eq!(result, $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $inputs:expr; $stricts:expr => err $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| TerminalColours::deduce(mf)) {
|
||||
assert_eq!(result.unwrap_err(), $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Default
|
||||
test!(empty: []; Both => Ok(TerminalColours::default()));
|
||||
|
||||
// --colour
|
||||
test!(u_always: ["--colour=always"]; Both => Ok(TerminalColours::Always));
|
||||
test!(u_auto: ["--colour", "auto"]; Both => Ok(TerminalColours::Automatic));
|
||||
test!(u_never: ["--colour=never"]; Both => Ok(TerminalColours::Never));
|
||||
|
||||
// --color
|
||||
test!(no_u_always: ["--color", "always"]; Both => Ok(TerminalColours::Always));
|
||||
test!(no_u_auto: ["--color=auto"]; Both => Ok(TerminalColours::Automatic));
|
||||
test!(no_u_never: ["--color", "never"]; Both => Ok(TerminalColours::Never));
|
||||
|
||||
// Errors
|
||||
test!(no_u_error: ["--color=upstream"]; Both => err Misfire::BadArgument(&flags::COLOR, OsString::from("upstream"))); // the error is for --color
|
||||
test!(u_error: ["--colour=lovers"]; Both => err Misfire::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one!
|
||||
|
||||
// Overriding
|
||||
test!(overridden_1: ["--colour=auto", "--colour=never"]; Last => Ok(TerminalColours::Never));
|
||||
test!(overridden_2: ["--color=auto", "--colour=never"]; Last => Ok(TerminalColours::Never));
|
||||
test!(overridden_3: ["--colour=auto", "--color=never"]; Last => Ok(TerminalColours::Never));
|
||||
test!(overridden_4: ["--color=auto", "--color=never"]; Last => Ok(TerminalColours::Never));
|
||||
|
||||
test!(overridden_5: ["--colour=auto", "--colour=never"]; Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("colour")));
|
||||
test!(overridden_6: ["--color=auto", "--colour=never"]; Complain => err Misfire::Duplicate(Flag::Long("color"), Flag::Long("colour")));
|
||||
test!(overridden_7: ["--colour=auto", "--color=never"]; Complain => err Misfire::Duplicate(Flag::Long("colour"), Flag::Long("color")));
|
||||
test!(overridden_8: ["--color=auto", "--color=never"]; Complain => err Misfire::Duplicate(Flag::Long("color"), Flag::Long("color")));
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod colour_test {
|
||||
use super::*;
|
||||
use options::flags;
|
||||
use options::parser::{Flag, Arg};
|
||||
|
||||
use options::test::parse_for_test;
|
||||
use options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR,
|
||||
&flags::COLOR_SCALE, &flags::COLOUR_SCALE ];
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $inputs:expr, $widther:expr; $stricts:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
|
||||
assert_eq!(result, $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $inputs:expr, $widther:expr; $stricts:expr => err $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
|
||||
assert_eq!(result.unwrap_err(), $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $inputs:expr, $widther:expr; $stricts:expr => like $pat:pat) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| Styles::deduce(mf, &None, &$widther).map(|s| s.colours)) {
|
||||
println!("Testing {:?}", result);
|
||||
match result {
|
||||
$pat => assert!(true),
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test!(width_1: ["--colour", "always"], || Some(80); Both => Ok(Colours::colourful(false)));
|
||||
test!(width_2: ["--colour", "always"], || None; Both => Ok(Colours::colourful(false)));
|
||||
test!(width_3: ["--colour", "never"], || Some(80); Both => Ok(Colours::plain()));
|
||||
test!(width_4: ["--colour", "never"], || None; Both => Ok(Colours::plain()));
|
||||
test!(width_5: ["--colour", "automatic"], || Some(80); Both => Ok(Colours::colourful(false)));
|
||||
test!(width_6: ["--colour", "automatic"], || None; Both => Ok(Colours::plain()));
|
||||
test!(width_7: [], || Some(80); Both => Ok(Colours::colourful(false)));
|
||||
test!(width_8: [], || None; Both => Ok(Colours::plain()));
|
||||
|
||||
test!(scale_1: ["--color=always", "--color-scale", "--colour-scale"], || None; Last => like Ok(Colours { scale: true, .. }));
|
||||
test!(scale_2: ["--color=always", "--color-scale", ], || None; Last => like Ok(Colours { scale: true, .. }));
|
||||
test!(scale_3: ["--color=always", "--colour-scale"], || None; Last => like Ok(Colours { scale: true, .. }));
|
||||
test!(scale_4: ["--color=always", ], || None; Last => like Ok(Colours { scale: false, .. }));
|
||||
|
||||
test!(scale_5: ["--color=always", "--color-scale", "--colour-scale"], || None; Complain => err Misfire::Duplicate(Flag::Long("color-scale"), Flag::Long("colour-scale")));
|
||||
test!(scale_6: ["--color=always", "--color-scale", ], || None; Complain => like Ok(Colours { scale: true, .. }));
|
||||
test!(scale_7: ["--color=always", "--colour-scale"], || None; Complain => like Ok(Colours { scale: true, .. }));
|
||||
test!(scale_8: ["--color=always", ], || None; Complain => like Ok(Colours { scale: false, .. }));
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod customs_test {
|
||||
use std::ffi::OsString;
|
||||
|
||||
use super::*;
|
||||
use options::Vars;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: ls $ls:expr, exa $exa:expr => colours $expected:ident -> $process_expected:expr) => {
|
||||
#[test]
|
||||
#[allow(unused_mut)]
|
||||
fn $name() {
|
||||
let mut $expected = Colours::colourful(false);
|
||||
$process_expected();
|
||||
|
||||
let vars = MockVars { ls: $ls, exa: $exa };
|
||||
|
||||
let mut result = Colours::colourful(false);
|
||||
let (_exts, _reset) = parse_color_vars(&vars, &mut result);
|
||||
assert_eq!($expected, result);
|
||||
}
|
||||
};
|
||||
($name:ident: ls $ls:expr, exa $exa:expr => exts $mappings:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mappings: Vec<(glob::Pattern, Style)>
|
||||
= $mappings.into_iter()
|
||||
.map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
|
||||
.collect();
|
||||
|
||||
let vars = MockVars { ls: $ls, exa: $exa };
|
||||
|
||||
let mut meh = Colours::colourful(false);
|
||||
let (result, _reset) = parse_color_vars(&vars, &mut meh);
|
||||
assert_eq!(ExtensionMappings { mappings }, result);
|
||||
}
|
||||
};
|
||||
($name:ident: ls $ls:expr, exa $exa:expr => colours $expected:ident -> $process_expected:expr, exts $mappings:expr) => {
|
||||
#[test]
|
||||
#[allow(unused_mut)]
|
||||
fn $name() {
|
||||
let mut $expected = Colours::colourful(false);
|
||||
$process_expected();
|
||||
|
||||
let mappings: Vec<(glob::Pattern, Style)>
|
||||
= $mappings.into_iter()
|
||||
.map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
|
||||
.collect();
|
||||
|
||||
let vars = MockVars { ls: $ls, exa: $exa };
|
||||
|
||||
let mut meh = Colours::colourful(false);
|
||||
let (result, _reset) = parse_color_vars(&vars, &mut meh);
|
||||
assert_eq!(ExtensionMappings { mappings }, result);
|
||||
assert_eq!($expected, meh);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct MockVars {
|
||||
ls: &'static str,
|
||||
exa: &'static str,
|
||||
}
|
||||
|
||||
// Test impl that just returns the value it has.
|
||||
impl Vars for MockVars {
|
||||
fn get(&self, name: &'static str) -> Option<OsString> {
|
||||
use options::vars;
|
||||
|
||||
if name == vars::LS_COLORS && !self.ls.is_empty() {
|
||||
OsString::from(self.ls.clone()).into()
|
||||
}
|
||||
else if name == vars::EXA_COLORS && !self.exa.is_empty() {
|
||||
OsString::from(self.exa.clone()).into()
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LS_COLORS can affect all of these colours:
|
||||
test!(ls_di: ls "di=31", exa "" => colours c -> { c.filekinds.directory = Red.normal(); });
|
||||
test!(ls_ex: ls "ex=32", exa "" => colours c -> { c.filekinds.executable = Green.normal(); });
|
||||
test!(ls_fi: ls "fi=33", exa "" => colours c -> { c.filekinds.normal = Yellow.normal(); });
|
||||
test!(ls_pi: ls "pi=34", exa "" => colours c -> { c.filekinds.pipe = Blue.normal(); });
|
||||
test!(ls_so: ls "so=35", exa "" => colours c -> { c.filekinds.socket = Purple.normal(); });
|
||||
test!(ls_bd: ls "bd=36", exa "" => colours c -> { c.filekinds.block_device = Cyan.normal(); });
|
||||
test!(ls_cd: ls "cd=35", exa "" => colours c -> { c.filekinds.char_device = Purple.normal(); });
|
||||
test!(ls_ln: ls "ln=34", exa "" => colours c -> { c.filekinds.symlink = Blue.normal(); });
|
||||
test!(ls_or: ls "or=33", exa "" => colours c -> { c.broken_symlink = Yellow.normal(); });
|
||||
|
||||
// EXA_COLORS can affect all those colours too:
|
||||
test!(exa_di: ls "", exa "di=32" => colours c -> { c.filekinds.directory = Green.normal(); });
|
||||
test!(exa_ex: ls "", exa "ex=33" => colours c -> { c.filekinds.executable = Yellow.normal(); });
|
||||
test!(exa_fi: ls "", exa "fi=34" => colours c -> { c.filekinds.normal = Blue.normal(); });
|
||||
test!(exa_pi: ls "", exa "pi=35" => colours c -> { c.filekinds.pipe = Purple.normal(); });
|
||||
test!(exa_so: ls "", exa "so=36" => colours c -> { c.filekinds.socket = Cyan.normal(); });
|
||||
test!(exa_bd: ls "", exa "bd=35" => colours c -> { c.filekinds.block_device = Purple.normal(); });
|
||||
test!(exa_cd: ls "", exa "cd=34" => colours c -> { c.filekinds.char_device = Blue.normal(); });
|
||||
test!(exa_ln: ls "", exa "ln=33" => colours c -> { c.filekinds.symlink = Yellow.normal(); });
|
||||
test!(exa_or: ls "", exa "or=32" => colours c -> { c.broken_symlink = Green.normal(); });
|
||||
|
||||
// EXA_COLORS will even override options from LS_COLORS:
|
||||
test!(ls_exa_di: ls "di=31", exa "di=32" => colours c -> { c.filekinds.directory = Green.normal(); });
|
||||
test!(ls_exa_ex: ls "ex=32", exa "ex=33" => colours c -> { c.filekinds.executable = Yellow.normal(); });
|
||||
test!(ls_exa_fi: ls "fi=33", exa "fi=34" => colours c -> { c.filekinds.normal = Blue.normal(); });
|
||||
|
||||
// But more importantly, EXA_COLORS has its own, special list of colours:
|
||||
test!(exa_ur: ls "", exa "ur=38;5;100" => colours c -> { c.perms.user_read = Fixed(100).normal(); });
|
||||
test!(exa_uw: ls "", exa "uw=38;5;101" => colours c -> { c.perms.user_write = Fixed(101).normal(); });
|
||||
test!(exa_ux: ls "", exa "ux=38;5;102" => colours c -> { c.perms.user_execute_file = Fixed(102).normal(); });
|
||||
test!(exa_ue: ls "", exa "ue=38;5;103" => colours c -> { c.perms.user_execute_other = Fixed(103).normal(); });
|
||||
test!(exa_gr: ls "", exa "gr=38;5;104" => colours c -> { c.perms.group_read = Fixed(104).normal(); });
|
||||
test!(exa_gw: ls "", exa "gw=38;5;105" => colours c -> { c.perms.group_write = Fixed(105).normal(); });
|
||||
test!(exa_gx: ls "", exa "gx=38;5;106" => colours c -> { c.perms.group_execute = Fixed(106).normal(); });
|
||||
test!(exa_tr: ls "", exa "tr=38;5;107" => colours c -> { c.perms.other_read = Fixed(107).normal(); });
|
||||
test!(exa_tw: ls "", exa "tw=38;5;108" => colours c -> { c.perms.other_write = Fixed(108).normal(); });
|
||||
test!(exa_tx: ls "", exa "tx=38;5;109" => colours c -> { c.perms.other_execute = Fixed(109).normal(); });
|
||||
test!(exa_su: ls "", exa "su=38;5;110" => colours c -> { c.perms.special_user_file = Fixed(110).normal(); });
|
||||
test!(exa_sf: ls "", exa "sf=38;5;111" => colours c -> { c.perms.special_other = Fixed(111).normal(); });
|
||||
test!(exa_xa: ls "", exa "xa=38;5;112" => colours c -> { c.perms.attribute = Fixed(112).normal(); });
|
||||
|
||||
test!(exa_sn: ls "", exa "sn=38;5;113" => colours c -> { c.size.numbers = Fixed(113).normal(); });
|
||||
test!(exa_sb: ls "", exa "sb=38;5;114" => colours c -> { c.size.unit = Fixed(114).normal(); });
|
||||
test!(exa_df: ls "", exa "df=38;5;115" => colours c -> { c.size.major = Fixed(115).normal(); });
|
||||
test!(exa_ds: ls "", exa "ds=38;5;116" => colours c -> { c.size.minor = Fixed(116).normal(); });
|
||||
|
||||
test!(exa_uu: ls "", exa "uu=38;5;117" => colours c -> { c.users.user_you = Fixed(117).normal(); });
|
||||
test!(exa_un: ls "", exa "un=38;5;118" => colours c -> { c.users.user_someone_else = Fixed(118).normal(); });
|
||||
test!(exa_gu: ls "", exa "gu=38;5;119" => colours c -> { c.users.group_yours = Fixed(119).normal(); });
|
||||
test!(exa_gn: ls "", exa "gn=38;5;120" => colours c -> { c.users.group_not_yours = Fixed(120).normal(); });
|
||||
|
||||
test!(exa_lc: ls "", exa "lc=38;5;121" => colours c -> { c.links.normal = Fixed(121).normal(); });
|
||||
test!(exa_lm: ls "", exa "lm=38;5;122" => colours c -> { c.links.multi_link_file = Fixed(122).normal(); });
|
||||
|
||||
test!(exa_ga: ls "", exa "ga=38;5;123" => colours c -> { c.git.new = Fixed(123).normal(); });
|
||||
test!(exa_gm: ls "", exa "gm=38;5;124" => colours c -> { c.git.modified = Fixed(124).normal(); });
|
||||
test!(exa_gd: ls "", exa "gd=38;5;125" => colours c -> { c.git.deleted = Fixed(125).normal(); });
|
||||
test!(exa_gv: ls "", exa "gv=38;5;126" => colours c -> { c.git.renamed = Fixed(126).normal(); });
|
||||
test!(exa_gt: ls "", exa "gt=38;5;127" => colours c -> { c.git.typechange = Fixed(127).normal(); });
|
||||
|
||||
test!(exa_xx: ls "", exa "xx=38;5;128" => colours c -> { c.punctuation = Fixed(128).normal(); });
|
||||
test!(exa_da: ls "", exa "da=38;5;129" => colours c -> { c.date = Fixed(129).normal(); });
|
||||
test!(exa_in: ls "", exa "in=38;5;130" => colours c -> { c.inode = Fixed(130).normal(); });
|
||||
test!(exa_bl: ls "", exa "bl=38;5;131" => colours c -> { c.blocks = Fixed(131).normal(); });
|
||||
test!(exa_hd: ls "", exa "hd=38;5;132" => colours c -> { c.header = Fixed(132).normal(); });
|
||||
test!(exa_lp: ls "", exa "lp=38;5;133" => colours c -> { c.symlink_path = Fixed(133).normal(); });
|
||||
test!(exa_cc: ls "", exa "cc=38;5;134" => colours c -> { c.control_char = Fixed(134).normal(); });
|
||||
test!(exa_bo: ls "", exa "bO=4" => colours c -> { c.broken_path_overlay = Style::default().underline(); });
|
||||
|
||||
// All the while, LS_COLORS treats them as filenames:
|
||||
test!(ls_uu: ls "uu=38;5;117", exa "" => exts [ ("uu", Fixed(117).normal()) ]);
|
||||
test!(ls_un: ls "un=38;5;118", exa "" => exts [ ("un", Fixed(118).normal()) ]);
|
||||
test!(ls_gu: ls "gu=38;5;119", exa "" => exts [ ("gu", Fixed(119).normal()) ]);
|
||||
test!(ls_gn: ls "gn=38;5;120", exa "" => exts [ ("gn", Fixed(120).normal()) ]);
|
||||
|
||||
// Just like all other keys:
|
||||
test!(ls_txt: ls "*.txt=31", exa "" => exts [ ("*.txt", Red.normal()) ]);
|
||||
test!(ls_mp3: ls "*.mp3=38;5;135", exa "" => exts [ ("*.mp3", Fixed(135).normal()) ]);
|
||||
test!(ls_mak: ls "Makefile=1;32;4", exa "" => exts [ ("Makefile", Green.bold().underline()) ]);
|
||||
test!(exa_txt: ls "", exa "*.zip=31" => exts [ ("*.zip", Red.normal()) ]);
|
||||
test!(exa_mp3: ls "", exa "lev.*=38;5;153" => exts [ ("lev.*", Fixed(153).normal()) ]);
|
||||
test!(exa_mak: ls "", exa "Cargo.toml=4;32;1" => exts [ ("Cargo.toml", Green.bold().underline()) ]);
|
||||
|
||||
// Testing whether a glob from EXA_COLORS overrides a glob from LS_COLORS
|
||||
// can’t be tested here, because they’ll both be added to the same vec
|
||||
|
||||
// Values get separated by colons:
|
||||
test!(ls_multi: ls "*.txt=31:*.rtf=32", exa "" => exts [ ("*.txt", Red.normal()), ("*.rtf", Green.normal()) ]);
|
||||
test!(exa_multi: ls "", exa "*.tmp=37:*.log=37" => exts [ ("*.tmp", White.normal()), ("*.log", White.normal()) ]);
|
||||
|
||||
test!(ls_five: ls "1*1=31:2*2=32:3*3=1;33:4*4=34;1:5*5=35;4", exa "" => exts [
|
||||
("1*1", Red.normal()), ("2*2", Green.normal()), ("3*3", Yellow.bold()), ("4*4", Blue.bold()), ("5*5", Purple.underline())
|
||||
]);
|
||||
|
||||
// Finally, colours get applied right-to-left:
|
||||
test!(ls_overwrite: ls "pi=31:pi=32:pi=33", exa "" => colours c -> { c.filekinds.pipe = Yellow.normal(); });
|
||||
test!(exa_overwrite: ls "", exa "da=36:da=35:da=34" => colours c -> { c.date = Blue.normal(); });
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
use crate::options::{flags, vars, Vars, OptionsError};
|
||||
use crate::options::parser::MatchedFlags;
|
||||
use crate::theme::{Options, UseColours, ColourScale, Definitions};
|
||||
|
||||
|
||||
impl Options {
|
||||
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
|
||||
let use_colours = UseColours::deduce(matches, vars)?;
|
||||
let colour_scale = ColourScale::deduce(matches)?;
|
||||
|
||||
let definitions = if use_colours == UseColours::Never {
|
||||
Definitions::default()
|
||||
}
|
||||
else {
|
||||
Definitions::deduce(vars)
|
||||
};
|
||||
|
||||
Ok(Self { use_colours, colour_scale, definitions })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl UseColours {
|
||||
fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
|
||||
let default_value = match vars.get(vars::NO_COLOR) {
|
||||
Some(_) => Self::Never,
|
||||
None => Self::Automatic,
|
||||
};
|
||||
|
||||
let word = match matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? {
|
||||
Some(w) => w,
|
||||
None => return Ok(default_value),
|
||||
};
|
||||
|
||||
if word == "always" {
|
||||
Ok(Self::Always)
|
||||
}
|
||||
else if word == "auto" || word == "automatic" {
|
||||
Ok(Self::Automatic)
|
||||
}
|
||||
else if word == "never" {
|
||||
Ok(Self::Never)
|
||||
}
|
||||
else {
|
||||
Err(OptionsError::BadArgument(&flags::COLOR, word.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ColourScale {
|
||||
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
|
||||
if matches.has_where(|f| f.matches(&flags::COLOR_SCALE) || f.matches(&flags::COLOUR_SCALE))?.is_some() {
|
||||
Ok(Self::Gradient)
|
||||
}
|
||||
else {
|
||||
Ok(Self::Fixed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Definitions {
|
||||
fn deduce<V: Vars>(vars: &V) -> Self {
|
||||
let ls = vars.get(vars::LS_COLORS) .map(|e| e.to_string_lossy().to_string());
|
||||
let exa = vars.get(vars::EXA_COLORS).map(|e| e.to_string_lossy().to_string());
|
||||
Self { ls, exa }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod terminal_test {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
use crate::options::flags;
|
||||
use crate::options::parser::{Flag, Arg};
|
||||
|
||||
use crate::options::test::parse_for_test;
|
||||
use crate::options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR,
|
||||
&flags::COLOR_SCALE, &flags::COLOUR_SCALE, ];
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
assert_eq!(result, $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $type:ident <- $inputs:expr, $env:expr; $stricts:expr => $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let env = $env;
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &env)) {
|
||||
assert_eq!(result, $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => err $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
assert_eq!(result.unwrap_err(), $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident: $type:ident <- $inputs:expr, $env:expr; $stricts:expr => err $result:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let env = $env;
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &env)) {
|
||||
assert_eq!(result.unwrap_err(), $result);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct MockVars {
|
||||
ls: &'static str,
|
||||
exa: &'static str,
|
||||
no_color: &'static str,
|
||||
}
|
||||
|
||||
impl MockVars {
|
||||
fn empty() -> MockVars {
|
||||
MockVars {
|
||||
ls: "",
|
||||
exa: "",
|
||||
no_color: "",
|
||||
}
|
||||
}
|
||||
fn with_no_color() -> MockVars {
|
||||
MockVars {
|
||||
ls: "",
|
||||
exa: "",
|
||||
no_color: "true",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test impl that just returns the value it has.
|
||||
impl Vars for MockVars {
|
||||
fn get(&self, name: &'static str) -> Option<OsString> {
|
||||
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() {
|
||||
Some(OsString::from(self.exa.clone()))
|
||||
}
|
||||
else if name == vars::NO_COLOR && ! self.no_color.is_empty() {
|
||||
Some(OsString::from(self.no_color.clone()))
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Default
|
||||
test!(empty: UseColours <- [], MockVars::empty(); Both => Ok(UseColours::Automatic));
|
||||
test!(empty_with_no_color: UseColours <- [], MockVars::with_no_color(); Both => Ok(UseColours::Never));
|
||||
|
||||
// --colour
|
||||
test!(u_always: UseColours <- ["--colour=always"], MockVars::empty(); Both => Ok(UseColours::Always));
|
||||
test!(u_auto: UseColours <- ["--colour", "auto"], MockVars::empty(); Both => Ok(UseColours::Automatic));
|
||||
test!(u_never: UseColours <- ["--colour=never"], MockVars::empty(); Both => Ok(UseColours::Never));
|
||||
|
||||
// --color
|
||||
test!(no_u_always: UseColours <- ["--color", "always"], MockVars::empty(); Both => Ok(UseColours::Always));
|
||||
test!(no_u_auto: UseColours <- ["--color=auto"], MockVars::empty(); Both => Ok(UseColours::Automatic));
|
||||
test!(no_u_never: UseColours <- ["--color", "never"], MockVars::empty(); Both => Ok(UseColours::Never));
|
||||
|
||||
// Errors
|
||||
test!(no_u_error: UseColours <- ["--color=upstream"], MockVars::empty(); Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("upstream"))); // the error is for --color
|
||||
test!(u_error: UseColours <- ["--colour=lovers"], MockVars::empty(); Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one!
|
||||
|
||||
// Overriding
|
||||
test!(overridden_1: UseColours <- ["--colour=auto", "--colour=never"], MockVars::empty(); Last => Ok(UseColours::Never));
|
||||
test!(overridden_2: UseColours <- ["--color=auto", "--colour=never"], MockVars::empty(); Last => Ok(UseColours::Never));
|
||||
test!(overridden_3: UseColours <- ["--colour=auto", "--color=never"], MockVars::empty(); Last => Ok(UseColours::Never));
|
||||
test!(overridden_4: UseColours <- ["--color=auto", "--color=never"], MockVars::empty(); Last => Ok(UseColours::Never));
|
||||
|
||||
test!(overridden_5: UseColours <- ["--colour=auto", "--colour=never"], MockVars::empty(); Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("colour")));
|
||||
test!(overridden_6: UseColours <- ["--color=auto", "--colour=never"], MockVars::empty(); Complain => err OptionsError::Duplicate(Flag::Long("color"), Flag::Long("colour")));
|
||||
test!(overridden_7: UseColours <- ["--colour=auto", "--color=never"], MockVars::empty(); Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("color")));
|
||||
test!(overridden_8: UseColours <- ["--color=auto", "--color=never"], MockVars::empty(); Complain => err OptionsError::Duplicate(Flag::Long("color"), Flag::Long("color")));
|
||||
|
||||
test!(scale_1: ColourScale <- ["--color-scale", "--colour-scale"]; Last => Ok(ColourScale::Gradient));
|
||||
test!(scale_2: ColourScale <- ["--color-scale", ]; Last => Ok(ColourScale::Gradient));
|
||||
test!(scale_3: ColourScale <- [ "--colour-scale"]; Last => Ok(ColourScale::Gradient));
|
||||
test!(scale_4: ColourScale <- [ ]; Last => Ok(ColourScale::Fixed));
|
||||
|
||||
test!(scale_5: ColourScale <- ["--color-scale", "--colour-scale"]; Complain => err OptionsError::Duplicate(Flag::Long("color-scale"), Flag::Long("colour-scale")));
|
||||
test!(scale_6: ColourScale <- ["--color-scale", ]; Complain => Ok(ColourScale::Gradient));
|
||||
test!(scale_7: ColourScale <- [ "--colour-scale"]; Complain => Ok(ColourScale::Gradient));
|
||||
test!(scale_8: ColourScale <- [ ]; Complain => Ok(ColourScale::Fixed));
|
||||
}
|
|
@ -15,10 +15,14 @@ pub static COLUMNS: &str = "COLUMNS";
|
|||
/// Environment variable used to datetime format.
|
||||
pub static TIME_STYLE: &str = "TIME_STYLE";
|
||||
|
||||
/// Environment variable used to disable colors.
|
||||
/// See: <https://no-color.org/>
|
||||
pub static NO_COLOR: &str = "NO_COLOR";
|
||||
|
||||
// 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";
|
||||
|
||||
|
@ -38,6 +42,11 @@ pub static EXA_DEBUG: &str = "EXA_DEBUG";
|
|||
/// number of rows of output.
|
||||
pub static EXA_GRID_ROWS: &str = "EXA_GRID_ROWS";
|
||||
|
||||
/// Environment variable used to specify how many spaces to print between an
|
||||
/// icon and its file name. Different terminals display icons differently,
|
||||
/// with 1 space bringing them too close together or 2 spaces putting them too
|
||||
/// far apart, so this may be necessary depending on how they are shown.
|
||||
pub static EXA_ICON_SPACING: &str = "EXA_ICON_SPACING";
|
||||
|
||||
|
||||
/// Mockable wrapper for `std::env::var_os`.
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
|
||||
use std::fmt;
|
||||
|
||||
use options::flags;
|
||||
use options::parser::MatchedFlags;
|
||||
use crate::options::flags;
|
||||
use crate::options::parser::MatchedFlags;
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct VersionString;
|
||||
// There were options here once, but there aren’t anymore!
|
||||
|
||||
|
@ -18,39 +18,40 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionString {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
write!(f, "{}", include!(concat!(env!("OUT_DIR"), "/version_string.txt")))
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(f, "{}", include_str!(concat!(env!("OUT_DIR"), "/version_string.txt")))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use 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,232 +1,217 @@
|
|||
use output::{View, Mode, grid, details, lines};
|
||||
use output::grid_details::{self, RowThreshold};
|
||||
use output::table::{TimeTypes, Environment, SizeFormat, Columns, Options as TableOptions};
|
||||
use output::time::TimeFormat;
|
||||
|
||||
use options::{flags, Misfire, Vars};
|
||||
use options::parser::MatchedFlags;
|
||||
|
||||
use fs::PlatformMetadata;
|
||||
use fs::feature::xattr;
|
||||
use crate::fs::feature::xattr;
|
||||
use crate::options::{flags, OptionsError, NumberSource, Vars};
|
||||
use crate::options::parser::MatchedFlags;
|
||||
use crate::output::{View, Mode, TerminalWidth, grid, details};
|
||||
use crate::output::grid_details::{self, RowThreshold};
|
||||
use crate::output::file_name::Options as FileStyle;
|
||||
use crate::output::table::{TimeTypes, SizeFormat, UserFormat, Columns, Options as TableOptions};
|
||||
use crate::output::time::TimeFormat;
|
||||
|
||||
|
||||
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> {
|
||||
use options::style::Styles;
|
||||
|
||||
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
|
||||
let mode = Mode::deduce(matches, vars)?;
|
||||
let Styles { colours, style } = Styles::deduce(matches, vars, || *TERM_WIDTH)?;
|
||||
Ok(View { mode, colours, style })
|
||||
let width = TerminalWidth::deduce(vars)?;
|
||||
let file_style = FileStyle::deduce(matches, vars)?;
|
||||
Ok(Self { mode, width, file_style })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Mode {
|
||||
|
||||
/// Determine the mode from the command-line arguments.
|
||||
pub fn deduce<V: Vars>(matches: &MatchedFlags, vars: &V) -> Result<Mode, Misfire> {
|
||||
use options::misfire::Misfire::*;
|
||||
/// Determine which viewing mode to use based on the user’s options.
|
||||
///
|
||||
/// As with the other options, arguments are scanned right-to-left and the
|
||||
/// first flag found is matched, so `exa --oneline --long` will pick a
|
||||
/// details view, and `exa --long --oneline` will pick the lines view.
|
||||
///
|
||||
/// This is complicated a little by the fact that `--grid` and `--tree`
|
||||
/// can also combine with `--long`, so care has to be taken to use the
|
||||
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
|
||||
let flag = matches.has_where_any(|f| f.matches(&flags::LONG) || f.matches(&flags::ONE_LINE)
|
||||
|| f.matches(&flags::GRID) || f.matches(&flags::TREE));
|
||||
|
||||
let long = || {
|
||||
if matches.has(&flags::ACROSS)? && !matches.has(&flags::GRID)? {
|
||||
Err(Useless(&flags::ACROSS, true, &flags::LONG))
|
||||
}
|
||||
else if matches.has(&flags::ONE_LINE)? {
|
||||
Err(Useless(&flags::ONE_LINE, true, &flags::LONG))
|
||||
}
|
||||
else {
|
||||
Ok(details::Options {
|
||||
table: Some(TableOptions::deduce(matches, vars)?),
|
||||
header: matches.has(&flags::HEADER)?,
|
||||
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
|
||||
icons: matches.has(&flags::ICONS)?,
|
||||
})
|
||||
}
|
||||
let flag = if let Some(f) = flag { f } else {
|
||||
Self::strict_check_long_flags(matches)?;
|
||||
let grid = grid::Options::deduce(matches)?;
|
||||
return Ok(Self::Grid(grid));
|
||||
};
|
||||
|
||||
let other_options_scan = || {
|
||||
if let Some(width) = TerminalWidth::deduce(vars)?.width() {
|
||||
if matches.has(&flags::ONE_LINE)? {
|
||||
if matches.has(&flags::ACROSS)? {
|
||||
Err(Useless(&flags::ACROSS, true, &flags::ONE_LINE))
|
||||
}
|
||||
else {
|
||||
let lines = lines::Options { icons: matches.has(&flags::ICONS)? };
|
||||
Ok(Mode::Lines(lines))
|
||||
}
|
||||
}
|
||||
else if matches.has(&flags::TREE)? {
|
||||
let details = details::Options {
|
||||
table: None,
|
||||
header: false,
|
||||
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
|
||||
icons: matches.has(&flags::ICONS)?,
|
||||
};
|
||||
if flag.matches(&flags::LONG)
|
||||
|| (flag.matches(&flags::TREE) && matches.has(&flags::LONG)?)
|
||||
|| (flag.matches(&flags::GRID) && matches.has(&flags::LONG)?)
|
||||
{
|
||||
let _ = matches.has(&flags::LONG)?;
|
||||
let details = details::Options::deduce_long(matches, vars)?;
|
||||
|
||||
Ok(Mode::Details(details))
|
||||
}
|
||||
else {
|
||||
let grid = grid::Options {
|
||||
across: matches.has(&flags::ACROSS)?,
|
||||
console_width: width,
|
||||
icons: matches.has(&flags::ICONS)?,
|
||||
};
|
||||
let flag = matches.has_where_any(|f| f.matches(&flags::GRID) || f.matches(&flags::TREE));
|
||||
|
||||
Ok(Mode::Grid(grid))
|
||||
}
|
||||
if flag.is_some() && flag.unwrap().matches(&flags::GRID) {
|
||||
let _ = matches.has(&flags::GRID)?;
|
||||
let grid = grid::Options::deduce(matches)?;
|
||||
let row_threshold = RowThreshold::deduce(vars)?;
|
||||
let grid_details = grid_details::Options { grid, details, row_threshold };
|
||||
return Ok(Self::GridDetails(grid_details));
|
||||
}
|
||||
|
||||
// If the terminal width couldn’t be matched for some reason, such
|
||||
// as the program’s stdout being connected to a file, then
|
||||
// fallback to the lines view.
|
||||
else if matches.has(&flags::TREE)? {
|
||||
let details = details::Options {
|
||||
table: None,
|
||||
header: false,
|
||||
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
|
||||
icons: matches.has(&flags::ICONS)?,
|
||||
};
|
||||
|
||||
Ok(Mode::Details(details))
|
||||
}
|
||||
else {
|
||||
let lines = lines::Options { icons: matches.has(&flags::ICONS)?, };
|
||||
Ok(Mode::Lines(lines))
|
||||
}
|
||||
};
|
||||
|
||||
if matches.has(&flags::LONG)? {
|
||||
let details = long()?;
|
||||
if matches.has(&flags::GRID)? {
|
||||
let other_options_mode = other_options_scan()?;
|
||||
if let Mode::Grid(grid) = other_options_mode {
|
||||
let row_threshold = RowThreshold::deduce(vars)?;
|
||||
return Ok(Mode::GridDetails(grid_details::Options { grid, details, row_threshold }));
|
||||
}
|
||||
else {
|
||||
return Ok(other_options_mode);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return Ok(Mode::Details(details));
|
||||
}
|
||||
// the --tree case is handled by the DirAction parser later
|
||||
return Ok(Self::Details(details));
|
||||
}
|
||||
|
||||
Self::strict_check_long_flags(matches)?;
|
||||
|
||||
if flag.matches(&flags::TREE) {
|
||||
let _ = matches.has(&flags::TREE)?;
|
||||
let details = details::Options::deduce_tree(matches)?;
|
||||
return Ok(Self::Details(details));
|
||||
}
|
||||
|
||||
if flag.matches(&flags::ONE_LINE) {
|
||||
let _ = matches.has(&flags::ONE_LINE)?;
|
||||
return Ok(Self::Lines);
|
||||
}
|
||||
|
||||
let grid = grid::Options::deduce(matches)?;
|
||||
Ok(Self::Grid(grid))
|
||||
}
|
||||
|
||||
fn strict_check_long_flags(matches: &MatchedFlags<'_>) -> Result<(), OptionsError> {
|
||||
// If --long hasn’t been passed, then check if we need to warn the
|
||||
// user about flags that won’t have any effect.
|
||||
if matches.is_strict() {
|
||||
for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS,
|
||||
&flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP ] {
|
||||
&flags::HEADER, &flags::BLOCKS, &flags::TIME, &flags::GROUP, &flags::NUMERIC ] {
|
||||
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 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.
|
||||
// There is an identical check in dir_action
|
||||
return Err(Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
|
||||
else if matches.has(&flags::LEVEL)? && ! matches.has(&flags::RECURSE)? && ! matches.has(&flags::TREE)? {
|
||||
return Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
|
||||
}
|
||||
}
|
||||
|
||||
other_options_scan()
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The width of the terminal requested by the user.
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum TerminalWidth {
|
||||
impl grid::Options {
|
||||
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
|
||||
let grid = grid::Options {
|
||||
across: matches.has(&flags::ACROSS)?,
|
||||
};
|
||||
|
||||
/// The user requested this specific number of columns.
|
||||
Set(usize),
|
||||
|
||||
/// The terminal was found to have this number of columns.
|
||||
Terminal(usize),
|
||||
|
||||
/// The user didn’t request any particular terminal width.
|
||||
Unset,
|
||||
Ok(grid)
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
use options::vars;
|
||||
impl details::Options {
|
||||
fn deduce_tree(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
|
||||
let details = details::Options {
|
||||
table: None,
|
||||
header: false,
|
||||
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
|
||||
};
|
||||
|
||||
Ok(details)
|
||||
}
|
||||
|
||||
fn deduce_long<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
|
||||
if matches.is_strict() {
|
||||
if matches.has(&flags::ACROSS)? && ! matches.has(&flags::GRID)? {
|
||||
return Err(OptionsError::Useless(&flags::ACROSS, true, &flags::LONG));
|
||||
}
|
||||
else if matches.has(&flags::ONE_LINE)? {
|
||||
return Err(OptionsError::Useless(&flags::ONE_LINE, true, &flags::LONG));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(details::Options {
|
||||
table: Some(TableOptions::deduce(matches, vars)?),
|
||||
header: matches.has(&flags::HEADER)?,
|
||||
xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TerminalWidth {
|
||||
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) => {
|
||||
let source = NumberSource::Env(vars::COLUMNS);
|
||||
Err(OptionsError::FailedParse(columns, source, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let Some(width) = *TERM_WIDTH {
|
||||
Ok(TerminalWidth::Terminal(width))
|
||||
}
|
||||
else {
|
||||
Ok(TerminalWidth::Unset)
|
||||
}
|
||||
}
|
||||
|
||||
fn width(&self) -> Option<usize> {
|
||||
match *self {
|
||||
TerminalWidth::Set(width) |
|
||||
TerminalWidth::Terminal(width) => Some(width),
|
||||
TerminalWidth::Unset => None,
|
||||
Ok(Self::Automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl RowThreshold {
|
||||
|
||||
/// Determine whether to use a row threshold based on the given
|
||||
/// environment variables.
|
||||
fn deduce<V: Vars>(vars: &V) -> Result<RowThreshold, Misfire> {
|
||||
use options::vars;
|
||||
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) => {
|
||||
let source = NumberSource::Env(vars::EXA_GRID_ROWS);
|
||||
Err(OptionsError::FailedParse(columns, source, 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 extra_columns = Columns::deduce(matches)?;
|
||||
Ok(TableOptions { env, time_format, size_format, extra_columns })
|
||||
let user_format = UserFormat::deduce(matches)?;
|
||||
let columns = Columns::deduce(matches)?;
|
||||
Ok(Self { size_format, time_format, user_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 = matches.has(&flags::GIT)?;
|
||||
|
||||
let blocks = matches.has(&flags::BLOCKS)?;
|
||||
let group = matches.has(&flags::GROUP)?;
|
||||
let inode = matches.has(&flags::INODE)?;
|
||||
let links = matches.has(&flags::LINKS)?;
|
||||
let octal = matches.has(&flags::OCTAL)?;
|
||||
|
||||
Ok(Columns { time_types, git, blocks, group, inode, links })
|
||||
let permissions = ! matches.has(&flags::NO_PERMISSIONS)?;
|
||||
let filesize = ! matches.has(&flags::NO_FILESIZE)?;
|
||||
let user = ! matches.has(&flags::NO_USER)?;
|
||||
|
||||
Ok(Self { time_types, inode, links, blocks, group, git, octal, permissions, filesize, user })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,13 +226,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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -256,39 +241,46 @@ 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 output::time::{DefaultFormat, ISOFormat};
|
||||
|
||||
let word = match matches.get(&flags::TIME_STYLE)? {
|
||||
Some(w) => w.to_os_string(),
|
||||
None => {
|
||||
use options::vars;
|
||||
fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
|
||||
let word =
|
||||
if let Some(w) = matches.get(&flags::TIME_STYLE)? {
|
||||
w.to_os_string()
|
||||
}
|
||||
else {
|
||||
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.into()))
|
||||
Err(OptionsError::BadArgument(&flags::TIME_STYLE, word))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl UserFormat {
|
||||
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
|
||||
let flag = matches.has(&flags::NUMERIC)?;
|
||||
Ok(if flag { Self::Numeric } else { Self::Name })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TimeTypes {
|
||||
|
||||
/// Determine which of a file’s time fields should be displayed for it
|
||||
|
@ -301,96 +293,75 @@ 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)?;
|
||||
let accessed = matches.has(&flags::ACCESSED)?;
|
||||
let created = matches.has(&flags::CREATED)?;
|
||||
|
||||
let time_types = if let Some(word) = possible_word {
|
||||
let no_time = matches.has(&flags::NO_TIME)?;
|
||||
|
||||
let time_types = if no_time {
|
||||
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()
|
||||
};
|
||||
|
||||
let mut fields = vec![];
|
||||
if time_types.modified { fields.push(PlatformMetadata::ModifiedTime); }
|
||||
if time_types.changed { fields.push(PlatformMetadata::ChangedTime); }
|
||||
if time_types.accessed { fields.push(PlatformMetadata::AccessedTime); }
|
||||
if time_types.created { fields.push(PlatformMetadata::CreatedTime); }
|
||||
|
||||
for field in fields {
|
||||
if let Err(misfire) = field.check_supported() {
|
||||
return Err(misfire);
|
||||
}
|
||||
}
|
||||
Ok(time_types)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Gets, then caches, the width of the terminal that exa is running in.
|
||||
// This gets used multiple times above, with no real guarantee of order,
|
||||
// so it’s easier to just cache it the first time it runs.
|
||||
lazy_static! {
|
||||
static ref TERM_WIDTH: Option<usize> = {
|
||||
// All of stdin, stdout, and stderr could not be connected to a
|
||||
// terminal, but we’re only interested in stdout because it’s
|
||||
// where the output goes.
|
||||
use term_size::dimensions_stdout;
|
||||
dimensions_stdout().map(|t| t.0)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use std::ffi::OsString;
|
||||
use options::flags;
|
||||
use options::parser::{Flag, Arg};
|
||||
use crate::options::flags;
|
||||
use crate::options::parser::{Flag, Arg};
|
||||
|
||||
use options::test::parse_for_test;
|
||||
use options::test::Strictnesses::*;
|
||||
use crate::options::test::parse_for_test;
|
||||
use crate::options::test::Strictnesses::*;
|
||||
|
||||
static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES, &flags::TIME_STYLE,
|
||||
&flags::TIME, &flags::MODIFIED, &flags::CHANGED,
|
||||
&flags::CREATED, &flags::ACCESSED, &flags::ICONS,
|
||||
&flags::CREATED, &flags::ACCESSED,
|
||||
&flags::HEADER, &flags::GROUP, &flags::INODE, &flags::GIT,
|
||||
&flags::LINKS, &flags::BLOCKS, &flags::LONG, &flags::LEVEL,
|
||||
&flags::GRID, &flags::ACROSS, &flags::ONE_LINE ];
|
||||
&flags::GRID, &flags::ACROSS, &flags::ONE_LINE, &flags::TREE,
|
||||
&flags::NUMERIC ];
|
||||
|
||||
macro_rules! test {
|
||||
|
||||
|
@ -407,7 +378,7 @@ mod test {
|
|||
|
||||
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => err $result:expr) => {
|
||||
/// Special macro for testing Err results.
|
||||
/// This is needed because sometimes the Ok type doesn’t implement PartialEq.
|
||||
/// This is needed because sometimes the Ok type doesn’t implement `PartialEq`.
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
|
@ -418,7 +389,7 @@ mod test {
|
|||
|
||||
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => like $pat:pat) => {
|
||||
/// More general macro for testing against a pattern.
|
||||
/// Instead of using PartialEq, this just tests if it matches a pat.
|
||||
/// Instead of using `PartialEq`, this just tests if it matches a pat.
|
||||
#[test]
|
||||
fn $name() {
|
||||
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
|
||||
|
@ -474,10 +445,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")));
|
||||
}
|
||||
|
||||
|
||||
|
@ -488,23 +459,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.
|
||||
|
@ -533,7 +504,7 @@ mod test {
|
|||
#[cfg(target_family = "unix")]
|
||||
test!(time_ch: TimeTypes <- ["--time=changed"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false }));
|
||||
#[cfg(target_family = "unix")]
|
||||
test!(t_ch: TimeTypes <- ["-t", "ch"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false }));
|
||||
test!(t_ch: TimeTypes <- ["-t", "ch"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false }));
|
||||
|
||||
// Accessed
|
||||
test!(acc: TimeTypes <- ["--accessed"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false }));
|
||||
|
@ -542,50 +513,43 @@ mod test {
|
|||
test!(time_a: TimeTypes <- ["-t", "acc"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false }));
|
||||
|
||||
// Created
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
test!(cr: TimeTypes <- ["--created"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true }));
|
||||
#[cfg(target_os = "linux")]
|
||||
test!(cr: TimeTypes <- ["--created"]; Both => err Misfire::Unsupported("creation time is not available on this platform currently".to_string()));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
test!(c: TimeTypes <- ["-U"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true }));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
test!(time_cr: TimeTypes <- ["--time=created"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true }));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
test!(t_cr: TimeTypes <- ["-tcr"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true }));
|
||||
|
||||
// Multiples
|
||||
test!(time_uu: TimeTypes <- ["-u", "--modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: true, created: false }));
|
||||
test!(time_uu: TimeTypes <- ["-u", "--modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: true, created: false }));
|
||||
|
||||
|
||||
// Errors
|
||||
test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("tea")));
|
||||
test!(t_ea: TimeTypes <- ["-tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("ea")));
|
||||
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')));
|
||||
}
|
||||
|
||||
|
||||
mod views {
|
||||
use super::*;
|
||||
use output::grid::Options as GridOptions;
|
||||
use output::lines::Options as LineOptions;
|
||||
|
||||
use crate::output::grid::Options as GridOptions;
|
||||
|
||||
|
||||
// Default
|
||||
test!(empty: Mode <- [], None; Both => like Ok(Mode::Grid(_)));
|
||||
|
||||
// Grid views
|
||||
test!(original_g: Mode <- ["-G"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, console_width: _, icons: _ })));
|
||||
test!(grid: Mode <- ["--grid"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, console_width: _, icons: _ })));
|
||||
test!(across: Mode <- ["--across"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, console_width: _, icons: _ })));
|
||||
test!(gracross: Mode <- ["-xG"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, console_width: _, icons: _ })));
|
||||
test!(icons: Mode <- ["--icons"], None; Both => like Ok(Mode::Grid(GridOptions { across: _, console_width: _, icons: true})));
|
||||
test!(original_g: Mode <- ["-G"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, .. })));
|
||||
test!(grid: Mode <- ["--grid"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, .. })));
|
||||
test!(across: Mode <- ["--across"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, .. })));
|
||||
test!(gracross: Mode <- ["-xG"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, .. })));
|
||||
|
||||
// Lines views
|
||||
test!(lines: Mode <- ["--oneline"], None; Both => like Ok(Mode::Lines(LineOptions{ icons: _ })));
|
||||
test!(prima: Mode <- ["-1"], None; Both => like Ok(Mode::Lines(LineOptions{ icons: _ })));
|
||||
test!(line_icon: Mode <- ["-1", "--icons"], None; Both => like Ok(Mode::Lines(LineOptions { icons: true })));
|
||||
test!(lines: Mode <- ["--oneline"], None; Both => like Ok(Mode::Lines));
|
||||
test!(prima: Mode <- ["-1"], None; Both => like Ok(Mode::Lines));
|
||||
|
||||
// Details views
|
||||
test!(long: Mode <- ["--long"], None; Both => like Ok(Mode::Details(_)));
|
||||
|
@ -595,28 +559,41 @@ mod test {
|
|||
test!(lid: Mode <- ["--long", "--grid"], None; Both => like Ok(Mode::GridDetails(_)));
|
||||
test!(leg: Mode <- ["-lG"], None; Both => like Ok(Mode::GridDetails(_)));
|
||||
|
||||
// Options that do nothing with --long
|
||||
test!(long_across: Mode <- ["--long", "--across"], None; Last => like Ok(Mode::Details(_)));
|
||||
|
||||
// Options that do nothing without --long
|
||||
test!(just_header: Mode <- ["--header"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_group: Mode <- ["--group"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_inode: Mode <- ["--inode"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_links: Mode <- ["--links"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_blocks: Mode <- ["--blocks"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_binary: Mode <- ["--binary"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_bytes: Mode <- ["--bytes"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_header: Mode <- ["--header"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_group: Mode <- ["--group"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_inode: Mode <- ["--inode"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_links: Mode <- ["--links"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_blocks: Mode <- ["--blocks"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_binary: Mode <- ["--binary"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_bytes: Mode <- ["--bytes"], None; Last => like Ok(Mode::Grid(_)));
|
||||
test!(just_numeric: Mode <- ["--numeric"], 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));
|
||||
test!(just_numeric2: Mode <- ["--numeric"], None; Complain => err OptionsError::Useless(&flags::NUMERIC, 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));
|
||||
|
||||
// Contradictions and combinations
|
||||
test!(lgo: Mode <- ["--long", "--grid", "--oneline"], None; Both => like Ok(Mode::Lines));
|
||||
test!(lgt: Mode <- ["--long", "--grid", "--tree"], None; Both => like Ok(Mode::Details(_)));
|
||||
test!(tgl: Mode <- ["--tree", "--grid", "--long"], None; Both => like Ok(Mode::GridDetails(_)));
|
||||
test!(tlg: Mode <- ["--tree", "--long", "--grid"], None; Both => like Ok(Mode::GridDetails(_)));
|
||||
test!(ot: Mode <- ["--oneline", "--tree"], None; Both => like Ok(Mode::Details(_)));
|
||||
test!(og: Mode <- ["--oneline", "--grid"], None; Both => like Ok(Mode::Grid(_)));
|
||||
test!(tg: Mode <- ["--tree", "--grid"], None; Both => like Ok(Mode::Grid(_)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
@ -77,11 +77,9 @@ impl TextCell {
|
|||
///
|
||||
/// This method allocates a `String` to hold the spaces.
|
||||
pub fn add_spaces(&mut self, count: usize) {
|
||||
use std::iter::repeat;
|
||||
|
||||
(*self.width) += count;
|
||||
|
||||
let spaces: String = repeat(' ').take(count).collect();
|
||||
let spaces: String = " ".repeat(count);
|
||||
self.contents.0.push(Style::default().paint(spaces));
|
||||
}
|
||||
|
||||
|
@ -92,7 +90,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 +134,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 +147,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.
|
||||
|
||||
|
@ -157,7 +155,7 @@ impl TextCellContents {
|
|||
|
||||
/// Produces an `ANSIStrings` value that can be used to print the styled
|
||||
/// values of this cell as an ANSI-terminal-formatted string.
|
||||
pub fn strings(&self) -> ANSIStrings {
|
||||
pub fn strings(&self) -> ANSIStrings<'_> {
|
||||
ANSIStrings(&self.0)
|
||||
}
|
||||
|
||||
|
@ -165,7 +163,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,23 +186,23 @@ 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
|
||||
/// of this type, and will `Deref` to the contained `usize` value.
|
||||
#[derive(PartialEq, Debug, Clone, Copy, Default)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
|
||||
pub struct DisplayWidth(usize);
|
||||
|
||||
impl<'a> From<&'a str> for DisplayWidth {
|
||||
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 +221,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 fs::{Dir, File};
|
||||
use fs::dir_action::RecurseOptions;
|
||||
use fs::filter::FileFilter;
|
||||
use fs::feature::ignore::IgnoreCache;
|
||||
use fs::feature::git::GitCache;
|
||||
use fs::feature::xattr::{Attribute, FileAttributes};
|
||||
use style::Colours;
|
||||
use output::cell::TextCell;
|
||||
use output::tree::{TreeTrunk, TreeParams, TreeDepth};
|
||||
use output::file_name::FileStyle;
|
||||
use output::table::{Table, Options as TableOptions, Row as TableRow};
|
||||
use output::icons::painted_icon;
|
||||
use ansi_term::Style;
|
||||
use scoped_threadpool::Pool;
|
||||
|
||||
use crate::fs::{Dir, File};
|
||||
use crate::fs::dir_action::RecurseOptions;
|
||||
use crate::fs::feature::git::GitCache;
|
||||
use crate::fs::feature::xattr::{Attribute, FileAttributes};
|
||||
use crate::fs::filter::FileFilter;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::output::file_name::Options as FileStyle;
|
||||
use crate::output::table::{Table, Options as TableOptions, Row as TableRow};
|
||||
use crate::output::tree::{TreeTrunk, TreeParams, TreeDepth};
|
||||
use crate::theme::Theme;
|
||||
|
||||
|
||||
/// With the **Details** view, the output gets formatted into columns, with
|
||||
/// each `Column` object showing some piece of information about the file,
|
||||
|
@ -92,7 +91,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, Eq, Debug)]
|
||||
pub struct Options {
|
||||
|
||||
/// Options specific to drawing a table.
|
||||
|
@ -104,20 +103,16 @@ 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
|
||||
pub icons: bool,
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub struct Render<'a> {
|
||||
pub dir: Option<&'a Dir>,
|
||||
pub files: Vec<File<'a>>,
|
||||
pub colours: &'a Colours,
|
||||
pub style: &'a FileStyle,
|
||||
pub theme: &'a Theme,
|
||||
pub file_style: &'a FileStyle,
|
||||
pub opts: &'a Options,
|
||||
|
||||
/// Whether to recurse through directories with a tree view, and if so,
|
||||
|
@ -127,16 +122,20 @@ pub struct Render<'a> {
|
|||
|
||||
/// How to sort and filter the files after getting their details.
|
||||
pub filter: &'a FileFilter,
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
impl<'a> AsRef<File<'a>> for Egg<'a> {
|
||||
|
@ -147,18 +146,22 @@ impl<'a> AsRef<File<'a>> for Egg<'a> {
|
|||
|
||||
|
||||
impl<'a> Render<'a> {
|
||||
pub fn render<W: Write>(self, mut git: Option<&'a GitCache>, ignore: Option<&'a IgnoreCache>, w: &mut W) -> IOResult<()> {
|
||||
let mut pool = Pool::new(num_cpus::get() as u32);
|
||||
pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
|
||||
let n_cpus = match num_cpus::get() as u32 {
|
||||
0 => 1,
|
||||
n => n,
|
||||
};
|
||||
let mut pool = Pool::new(n_cpus);
|
||||
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.theme);
|
||||
|
||||
if self.opts.header {
|
||||
let header = table.header_row();
|
||||
|
@ -169,14 +172,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, ignore, 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, ignore, 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())?
|
||||
|
@ -188,17 +191,18 @@ 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>], ignore: Option<&'ig IgnoreCache>, 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 fs::feature::xattr;
|
||||
use log::*;
|
||||
use crate::fs::feature::xattr;
|
||||
|
||||
let mut file_eggs = Vec::new();
|
||||
let mut file_eggs = (0..src.len()).map(|_| MaybeUninit::uninit()).collect::<Vec<_>>();
|
||||
|
||||
pool.scoped(|scoped| {
|
||||
let file_eggs = Arc::new(Mutex::new(&mut file_eggs));
|
||||
let table = table.as_ref();
|
||||
|
||||
for file in src {
|
||||
for (idx, file) in src.iter().enumerate() {
|
||||
let file_eggs = Arc::clone(&file_eggs);
|
||||
|
||||
scoped.execute(move || {
|
||||
|
@ -243,72 +247,73 @@ 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 egg = Egg { table_row, xattrs, errors, dir, file, icon };
|
||||
file_eggs.lock().unwrap().push(egg);
|
||||
let egg = Egg { table_row, xattrs, errors, dir, file };
|
||||
unsafe { std::ptr::write(file_eggs.lock().unwrap()[idx].as_mut_ptr(), egg) }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// this is safe because all entries have been initialized above
|
||||
let mut file_eggs = unsafe { std::mem::transmute::<_, Vec<Egg<'_>>>(file_eggs) };
|
||||
self.filter.sort_files(&mut file_eggs);
|
||||
|
||||
for (tree_params, egg) in depth.iterate_over(file_eggs.into_iter()) {
|
||||
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);
|
||||
}
|
||||
|
||||
let mut name_cell = TextCell::default();
|
||||
if let Some(icon) = egg.icon {
|
||||
name_cell.push(ANSIGenericString::from(icon), 2)
|
||||
}
|
||||
name_cell.append(self.style.for_file(&egg.file, self.colours)
|
||||
.with_link_paths()
|
||||
.paint()
|
||||
.promote());
|
||||
|
||||
let file_name = self.file_style.for_file(egg.file, self.theme)
|
||||
.with_link_paths()
|
||||
.paint()
|
||||
.promote();
|
||||
|
||||
let row = Row {
|
||||
tree: tree_params,
|
||||
cells: egg.table_row,
|
||||
name: name_cell,
|
||||
name: file_name,
|
||||
};
|
||||
|
||||
rows.push(row);
|
||||
|
||||
if let Some(ref dir) = egg.dir {
|
||||
for file_to_add in dir.files(self.filter.dot_filter, ignore) {
|
||||
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)));
|
||||
}
|
||||
|
@ -317,19 +322,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, ignore, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -338,26 +347,27 @@ impl<'a> Render<'a> {
|
|||
Row {
|
||||
tree: TreeParams::new(TreeDepth::root(), false),
|
||||
cells: Some(header),
|
||||
name: TextCell::paint_str(self.colours.header, "Name"),
|
||||
name: TextCell::paint_str(self.theme.ui.header, "Name"),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_error(&self, error: &IOError, tree: TreeParams, path: Option<PathBuf>) -> Row {
|
||||
use output::file_name::Colours;
|
||||
fn render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row {
|
||||
use crate::output::file_name::Colours;
|
||||
|
||||
let error_message = match path {
|
||||
Some(path) => format!("<{}: {}>", path.display(), error),
|
||||
None => format!("<{}>", error),
|
||||
let error_message = if let Some(path) = path {
|
||||
format!("<{}: {}>", path.display(), error)
|
||||
} else {
|
||||
format!("<{}>", error)
|
||||
};
|
||||
|
||||
// TODO: broken_symlink() doesn’t quite seem like the right name for
|
||||
// the style that’s being used here. Maybe split it in two?
|
||||
let name = TextCell::paint(self.colours.broken_symlink(), error_message);
|
||||
let name = TextCell::paint(self.theme.broken_symlink(), error_message);
|
||||
Row { cells: None, name, tree }
|
||||
}
|
||||
|
||||
fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
|
||||
let name = TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size));
|
||||
let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{} (len {})", xattr.name, xattr.size));
|
||||
Row { cells: None, name, tree }
|
||||
}
|
||||
|
||||
|
@ -371,7 +381,7 @@ impl<'a> Render<'a> {
|
|||
total_width: table.widths().total(),
|
||||
table,
|
||||
inner: rows.into_iter(),
|
||||
tree_style: self.colours.punctuation,
|
||||
tree_style: self.theme.ui.punctuation,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -379,7 +389,7 @@ impl<'a> Render<'a> {
|
|||
Iter {
|
||||
tree_trunk: TreeTrunk::default(),
|
||||
inner: rows.into_iter(),
|
||||
tree_style: self.colours.punctuation,
|
||||
tree_style: self.theme.ui.punctuation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -389,13 +399,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,
|
||||
|
||||
|
@ -434,7 +444,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);
|
||||
}
|
||||
|
||||
|
@ -464,7 +474,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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
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) {
|
||||
pub fn escape(string: String, bits: &mut Vec<ANSIString<'_>>, 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 {
|
||||
// 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,41 +1,42 @@
|
|||
use std::fmt::Debug;
|
||||
use std::path::Path;
|
||||
|
||||
use ansi_term::{ANSIString, Style};
|
||||
|
||||
use fs::{File, FileTarget};
|
||||
use output::escape;
|
||||
use output::cell::TextCellContents;
|
||||
use output::render::FiletypeColours;
|
||||
use crate::fs::{File, FileTarget};
|
||||
use crate::output::cell::TextCellContents;
|
||||
use crate::output::escape;
|
||||
use crate::output::icons::{icon_for_file, iconify_style};
|
||||
use crate::output::render::FiletypeColours;
|
||||
|
||||
|
||||
/// Basically a file name factory.
|
||||
#[derive(Debug)]
|
||||
pub struct FileStyle {
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Options {
|
||||
|
||||
/// Whether to append file class characters to file names.
|
||||
pub classify: Classify,
|
||||
|
||||
/// Mapping of file extensions to colours, to highlight regular files.
|
||||
pub exts: Box<FileColours>,
|
||||
/// Whether to prepend icon characters before file names.
|
||||
pub show_icons: ShowIcons,
|
||||
}
|
||||
|
||||
impl FileStyle {
|
||||
impl Options {
|
||||
|
||||
/// Create a new `FileName` that prints the given file’s name, painting it
|
||||
/// 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> {
|
||||
pub fn for_file<'a, 'dir, C>(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,
|
||||
options: self,
|
||||
target: if file.is_link() { Some(file.link_target()) }
|
||||
else { None }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// When displaying a file name, there needs to be some way to handle broken
|
||||
/// links, depending on how long the resulting Cell can be.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
|
@ -53,7 +54,7 @@ enum LinkStyle {
|
|||
|
||||
|
||||
/// Whether to append file class characters to the file names.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum Classify {
|
||||
|
||||
/// Just display the file names, without any characters.
|
||||
|
@ -65,16 +66,28 @@ pub enum Classify {
|
|||
}
|
||||
|
||||
impl Default for Classify {
|
||||
fn default() -> Classify {
|
||||
Classify::JustFilenames
|
||||
fn default() -> Self {
|
||||
Self::JustFilenames
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Whether and how to show icons.
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum ShowIcons {
|
||||
|
||||
/// Don’t show icons at all.
|
||||
Off,
|
||||
|
||||
/// Show icons next to file names, with the given number of spaces between
|
||||
/// the icon and the file name.
|
||||
On(u32),
|
||||
}
|
||||
|
||||
|
||||
/// 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> {
|
||||
pub struct FileName<'a, 'dir, C> {
|
||||
|
||||
/// A reference to the file that we’re getting the name of.
|
||||
file: &'a File<'dir>,
|
||||
|
@ -83,20 +96,15 @@ pub struct FileName<'a, 'dir: 'a, C: Colours+'a> {
|
|||
colours: &'a C,
|
||||
|
||||
/// The file that this file points to if it’s a link.
|
||||
target: Option<FileTarget<'dir>>,
|
||||
target: Option<FileTarget<'dir>>, // todo: remove?
|
||||
|
||||
/// How to handle displaying links.
|
||||
link_style: LinkStyle,
|
||||
|
||||
/// Whether to append file class characters to file names.
|
||||
classify: Classify,
|
||||
|
||||
/// Mapping of file extensions to colours, to highlight regular files.
|
||||
exts: &'a FileColours,
|
||||
options: Options,
|
||||
}
|
||||
|
||||
|
||||
impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
||||
impl<'a, 'dir, C> FileName<'a, 'dir, C> {
|
||||
|
||||
/// Sets the flag on this file name to display link targets with an
|
||||
/// arrow followed by their path.
|
||||
|
@ -104,6 +112,9 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
|||
self.link_style = LinkStyle::FullLinkPaths;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
||||
|
||||
/// Paints the name of the file using the colours, resulting in a vector
|
||||
/// of coloured cells that can be printed to the terminal.
|
||||
|
@ -114,15 +125,28 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
|||
pub fn paint(&self) -> TextCellContents {
|
||||
let mut bits = Vec::new();
|
||||
|
||||
if let ShowIcons::On(spaces_count) = self.options.show_icons {
|
||||
let style = iconify_style(self.style());
|
||||
let file_icon = icon_for_file(self.file).to_string();
|
||||
|
||||
bits.push(style.paint(file_icon));
|
||||
|
||||
match spaces_count {
|
||||
1 => bits.push(style.paint(" ")),
|
||||
2 => bits.push(style.paint(" ")),
|
||||
n => bits.push(style.paint(spaces(n))),
|
||||
}
|
||||
}
|
||||
|
||||
if self.file.parent_dir.is_none() {
|
||||
if let Some(parent) = self.file.path.parent() {
|
||||
self.add_parent_bits(&mut bits, parent);
|
||||
}
|
||||
}
|
||||
|
||||
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 +157,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,36 +167,52 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
|||
self.add_parent_bits(&mut bits, parent);
|
||||
}
|
||||
|
||||
if !target.name.is_empty() {
|
||||
let target = FileName {
|
||||
if ! target.name.is_empty() {
|
||||
let target_options = Options {
|
||||
classify: Classify::JustFilenames,
|
||||
show_icons: ShowIcons::Off,
|
||||
};
|
||||
|
||||
let target_name = FileName {
|
||||
file: target,
|
||||
colours: self.colours,
|
||||
target: None,
|
||||
link_style: LinkStyle::FullLinkPaths,
|
||||
classify: Classify::JustFilenames,
|
||||
exts: self.exts,
|
||||
options: target_options,
|
||||
};
|
||||
|
||||
for bit in target.coloured_file_name() {
|
||||
for bit in target_name.coloured_file_name() {
|
||||
bits.push(bit);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
FileTarget::Broken(ref broken_path) => {
|
||||
if let Classify::AddFileIndicators = self.options.classify {
|
||||
if let Some(class) = self.classify_char(target) {
|
||||
bits.push(Style::default().paint(class));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if let Some(class) = self.classify_char() {
|
||||
else if let Classify::AddFileIndicators = self.options.classify {
|
||||
if let Some(class) = self.classify_char(self.file) {
|
||||
bits.push(Style::default().paint(class));
|
||||
}
|
||||
}
|
||||
|
@ -180,40 +220,61 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
|||
bits.into()
|
||||
}
|
||||
|
||||
|
||||
/// Adds the bits of the parent path to the given bits vector.
|
||||
/// The path gets its characters escaped based on the colours.
|
||||
fn add_parent_bits(&self, bits: &mut Vec<ANSIString>, parent: &Path) {
|
||||
fn add_parent_bits(&self, bits: &mut Vec<ANSIString<'_>>, parent: &Path) {
|
||||
let coconut = parent.components().count();
|
||||
|
||||
if coconut == 1 && parent.has_root() {
|
||||
bits.push(self.colours.symlink_path().paint("/"));
|
||||
bits.push(self.colours.symlink_path().paint(std::path::MAIN_SEPARATOR.to_string()));
|
||||
}
|
||||
else if coconut >= 1 {
|
||||
escape(parent.to_string_lossy().to_string(), bits, self.colours.symlink_path(), self.colours.control_char());
|
||||
bits.push(self.colours.symlink_path().paint("/"));
|
||||
escape(
|
||||
parent.to_string_lossy().to_string(),
|
||||
bits,
|
||||
self.colours.symlink_path(),
|
||||
self.colours.control_char(),
|
||||
);
|
||||
bits.push(self.colours.symlink_path().paint(std::path::MAIN_SEPARATOR.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The character to be displayed after a file when classifying is on, if
|
||||
/// the file’s type has one associated with it.
|
||||
fn classify_char(&self) -> Option<&'static str> {
|
||||
if self.file.is_executable_file() {
|
||||
#[cfg(unix)]
|
||||
fn classify_char(&self, file: &File<'_>) -> Option<&'static str> {
|
||||
if file.is_executable_file() {
|
||||
Some("*")
|
||||
} else if self.file.is_directory() {
|
||||
}
|
||||
else if file.is_directory() {
|
||||
Some("/")
|
||||
} else if self.file.is_pipe() {
|
||||
}
|
||||
else if file.is_pipe() {
|
||||
Some("|")
|
||||
} else if self.file.is_link() {
|
||||
}
|
||||
else if file.is_link() {
|
||||
Some("@")
|
||||
} else if self.file.is_socket() {
|
||||
}
|
||||
else if file.is_socket() {
|
||||
Some("=")
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn classify_char(&self, file: &File<'_>) -> Option<&'static str> {
|
||||
if file.is_directory() {
|
||||
Some("/")
|
||||
}
|
||||
else if file.is_link() {
|
||||
Some("@")
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns at least one ANSI-highlighted string representing this file’s
|
||||
/// name using the given set of colours.
|
||||
|
@ -228,13 +289,19 @@ 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 {
|
||||
|
@ -246,23 +313,22 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
|||
}
|
||||
}
|
||||
|
||||
self.kind_style()
|
||||
.or_else(|| self.exts.colour_file(self.file))
|
||||
.unwrap_or_else(|| self.colours.normal())
|
||||
}
|
||||
|
||||
fn kind_style(&self) -> Option<Style> {
|
||||
Some(match self.file {
|
||||
match self.file {
|
||||
f if f.is_directory() => self.colours.directory(),
|
||||
#[cfg(unix)]
|
||||
f if f.is_executable_file() => self.colours.executable_file(),
|
||||
f if f.is_link() => self.colours.symlink(),
|
||||
#[cfg(unix)]
|
||||
f if f.is_pipe() => self.colours.pipe(),
|
||||
#[cfg(unix)]
|
||||
f if f.is_block_device() => self.colours.block_device(),
|
||||
#[cfg(unix)]
|
||||
f if f.is_char_device() => self.colours.char_device(),
|
||||
#[cfg(unix)]
|
||||
f if f.is_socket() => self.colours.socket(),
|
||||
f if !f.is_file() => self.colours.special(),
|
||||
_ => return None,
|
||||
})
|
||||
f if ! f.is_file() => self.colours.special(),
|
||||
_ => self.colours.colour_file(self.file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -294,30 +360,12 @@ pub trait Colours: FiletypeColours {
|
|||
|
||||
/// The style to paint a file that has its executable bit set.
|
||||
fn executable_file(&self) -> Style;
|
||||
|
||||
fn colour_file(&self, file: &File<'_>) -> Style;
|
||||
}
|
||||
|
||||
|
||||
// needs Debug because FileStyle derives it
|
||||
use std::fmt::Debug;
|
||||
use std::marker::Sync;
|
||||
pub trait FileColours: Debug+Sync {
|
||||
fn colour_file(&self, file: &File) -> Option<Style>;
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct NoFileColours;
|
||||
impl FileColours for NoFileColours {
|
||||
fn colour_file(&self, _file: &File) -> Option<Style> { None }
|
||||
}
|
||||
|
||||
// When getting the colour of a file from a *pair* of colourisers, try the
|
||||
// first one then try the second one. This lets the user provide their own
|
||||
// file type associations, while falling back to the default set if not set
|
||||
// explicitly.
|
||||
impl<A, B> FileColours for (A, B)
|
||||
where A: FileColours, B: FileColours {
|
||||
fn colour_file(&self, file: &File) -> Option<Style> {
|
||||
self.0.colour_file(file).or_else(|| self.1.colour_file(file))
|
||||
}
|
||||
/// Generate a string made of `n` spaces.
|
||||
fn spaces(width: u32) -> String {
|
||||
(0 .. width).into_iter().map(|_| ' ').collect()
|
||||
}
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
use std::io::{Write, Result as IOResult};
|
||||
use std::io::{self, Write};
|
||||
|
||||
use term_grid as tg;
|
||||
|
||||
use fs::File;
|
||||
use style::Colours;
|
||||
use output::file_name::FileStyle;
|
||||
use output::icons::painted_icon;
|
||||
use output::cell::DisplayWidth;
|
||||
use crate::fs::File;
|
||||
use crate::fs::filter::FileFilter;
|
||||
use crate::output::file_name::Options as FileStyle;
|
||||
use crate::theme::Theme;
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct Options {
|
||||
pub across: bool,
|
||||
pub console_width: usize,
|
||||
pub icons: bool,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn direction(&self) -> tg::Direction {
|
||||
pub fn direction(self) -> tg::Direction {
|
||||
if self.across { tg::Direction::LeftToRight }
|
||||
else { tg::Direction::TopToBottom }
|
||||
}
|
||||
|
@ -26,13 +23,15 @@ impl Options {
|
|||
|
||||
pub struct Render<'a> {
|
||||
pub files: Vec<File<'a>>,
|
||||
pub colours: &'a Colours,
|
||||
pub style: &'a FileStyle,
|
||||
pub theme: &'a Theme,
|
||||
pub file_style: &'a FileStyle,
|
||||
pub opts: &'a Options,
|
||||
pub console_width: usize,
|
||||
pub filter: &'a FileFilter,
|
||||
}
|
||||
|
||||
impl<'a> Render<'a> {
|
||||
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
|
||||
pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
|
||||
let mut grid = tg::Grid::new(tg::GridOptions {
|
||||
direction: self.opts.direction(),
|
||||
filling: tg::Filling::Spaces(2),
|
||||
|
@ -40,22 +39,18 @@ impl<'a> Render<'a> {
|
|||
|
||||
grid.reserve(self.files.len());
|
||||
|
||||
self.filter.sort_files(&mut self.files);
|
||||
for file in &self.files {
|
||||
let icon = if self.opts.icons { Some(painted_icon(&file, &self.style)) } else { None };
|
||||
let filename = self.style.for_file(file, self.colours).paint();
|
||||
let width = if self.opts.icons {
|
||||
DisplayWidth::from(2) + filename.width()
|
||||
} else {
|
||||
filename.width()
|
||||
};
|
||||
let filename = self.file_style.for_file(file, self.theme).paint();
|
||||
|
||||
grid.add(tg::Cell {
|
||||
contents: format!("{icon}{filename}", icon=&icon.unwrap_or("".to_string()), filename=filename.strings().to_string()),
|
||||
width: *width,
|
||||
contents: filename.strings().to_string(),
|
||||
width: *filename.width(),
|
||||
alignment: tg::Alignment::Left,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(display) = grid.fit_into_width(self.opts.console_width) {
|
||||
if let Some(display) = grid.fit_into_width(self.console_width) {
|
||||
write!(w, "{}", display)
|
||||
}
|
||||
else {
|
||||
|
@ -63,9 +58,10 @@ impl<'a> Render<'a> {
|
|||
// This isn’t *quite* the same as the lines view, which also
|
||||
// displays full link paths.
|
||||
for file in &self.files {
|
||||
let name_cell = self.style.for_file(file, self.colours).paint();
|
||||
let name_cell = self.file_style.for_file(file, self.theme).paint();
|
||||
writeln!(w, "{}", name_cell.strings())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,37 @@
|
|||
//! 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 ansi_term::ANSIStrings;
|
||||
use term_grid as grid;
|
||||
|
||||
use fs::{Dir, File};
|
||||
use fs::feature::git::GitCache;
|
||||
use fs::feature::xattr::FileAttributes;
|
||||
use fs::filter::FileFilter;
|
||||
use crate::fs::{Dir, File};
|
||||
use crate::fs::feature::git::GitCache;
|
||||
use crate::fs::feature::xattr::FileAttributes;
|
||||
use crate::fs::filter::FileFilter;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
|
||||
use crate::output::file_name::Options as FileStyle;
|
||||
use crate::output::grid::Options as GridOptions;
|
||||
use crate::output::table::{Table, Row as TableRow, Options as TableOptions};
|
||||
use crate::output::tree::{TreeParams, TreeDepth};
|
||||
use crate::theme::Theme;
|
||||
|
||||
use style::Colours;
|
||||
use output::cell::TextCell;
|
||||
use output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
|
||||
use output::grid::Options as GridOptions;
|
||||
use output::file_name::FileStyle;
|
||||
use output::table::{Table, Row as TableRow, Options as TableOptions};
|
||||
use output::tree::{TreeParams, TreeDepth};
|
||||
use output::icons::painted_icon;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Options {
|
||||
pub grid: GridOptions,
|
||||
pub details: DetailsOptions,
|
||||
pub row_threshold: RowThreshold,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub fn to_details_options(&self) -> &DetailsOptions {
|
||||
&self.details
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The grid-details view can be configured to revert to just a details view
|
||||
/// (with one column) if it wouldn’t produce enough rows of output.
|
||||
///
|
||||
|
@ -33,7 +39,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, Eq, Debug, Copy, Clone)]
|
||||
pub enum RowThreshold {
|
||||
|
||||
/// Only use grid-details view if it would result in at least this many
|
||||
|
@ -56,10 +62,10 @@ pub struct Render<'a> {
|
|||
pub files: Vec<File<'a>>,
|
||||
|
||||
/// How to colour various pieces of text.
|
||||
pub colours: &'a Colours,
|
||||
pub theme: &'a Theme,
|
||||
|
||||
/// How to format filenames.
|
||||
pub style: &'a FileStyle,
|
||||
pub file_style: &'a FileStyle,
|
||||
|
||||
/// The grid part of the grid-details view.
|
||||
pub grid: &'a GridOptions,
|
||||
|
@ -75,119 +81,128 @@ pub struct Render<'a> {
|
|||
/// The minimum number of rows that there need to be before grid-details
|
||||
/// mode is activated.
|
||||
pub row_threshold: RowThreshold,
|
||||
|
||||
/// Whether we are skipping Git-ignored files.
|
||||
pub git_ignoring: bool,
|
||||
|
||||
pub git: Option<&'a GitCache>,
|
||||
|
||||
pub console_width: usize,
|
||||
}
|
||||
|
||||
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> {
|
||||
fn details_for_column(&self) -> DetailsRender<'a> {
|
||||
DetailsRender {
|
||||
dir: self.dir,
|
||||
files: Vec::new(),
|
||||
colours: self.colours,
|
||||
style: self.style,
|
||||
opts: self.details,
|
||||
recurse: None,
|
||||
filter: self.filter,
|
||||
dir: self.dir,
|
||||
files: Vec::new(),
|
||||
theme: self.theme,
|
||||
file_style: self.file_style,
|
||||
opts: self.details,
|
||||
recurse: None,
|
||||
filter: self.filter,
|
||||
git_ignoring: self.git_ignoring,
|
||||
git: self.git,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Details render for when this grid-details render doesn’t fit
|
||||
/// in the terminal (or something has gone wrong) and we have given up.
|
||||
/// in the terminal (or something has gone wrong) and we have given up, or
|
||||
/// when the user asked for a grid-details view but the terminal width is
|
||||
/// not available, so we downgrade.
|
||||
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,
|
||||
dir: self.dir,
|
||||
files: self.files,
|
||||
theme: self.theme,
|
||||
file_style: self.file_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, None, 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 drender = self.details_for_column();
|
||||
|
||||
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);
|
||||
let file_cell = self.style.for_file(file, self.colours).paint().promote();
|
||||
icon_cell.append(file_cell);
|
||||
icon_cell
|
||||
} else {
|
||||
self.style.for_file(file, self.colours).paint().promote()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<TextCell>>();
|
||||
.map(|file| self.file_style.for_file(file, self.theme).paint().promote())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut last_working_table = self.make_grid(1, options, git, &file_names, rows.clone(), &drender);
|
||||
let mut last_working_grid = self.make_grid(1, options, &file_names, rows.clone(), &drender);
|
||||
|
||||
if file_names.len() == 1 {
|
||||
return Some((last_working_grid, 1));
|
||||
}
|
||||
|
||||
// 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);
|
||||
d.is_complete() && d.width() <= self.grid.console_width
|
||||
d.width() <= self.console_width
|
||||
};
|
||||
|
||||
if the_grid_fits {
|
||||
last_working_table = grid;
|
||||
last_working_grid = grid;
|
||||
}
|
||||
else {
|
||||
// If we’ve figured out how many columns can fit in the user’s
|
||||
// terminal, and it turns out there aren’t enough rows to
|
||||
// make it worthwhile, then just resort to the lines view.
|
||||
|
||||
if !the_grid_fits || column_count == file_names.len() {
|
||||
let last_column_count = if the_grid_fits { column_count } else { column_count - 1 };
|
||||
// If we’ve figured out how many columns can fit in the user’s terminal,
|
||||
// and it turns out there aren’t enough rows to make it worthwhile
|
||||
// (according to EXA_GRID_ROWS), then just resort to the lines view.
|
||||
if let RowThreshold::MinimumRows(thresh) = self.row_threshold {
|
||||
if last_working_table.fit_into_columns(column_count - 1).row_count() < thresh {
|
||||
if last_working_grid.fit_into_columns(last_column_count).row_count() < thresh {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
return Some((last_working_table, column_count - 1));
|
||||
return Some((last_working_grid, last_column_count));
|
||||
}
|
||||
}
|
||||
|
||||
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.theme);
|
||||
let mut rows = Vec::new();
|
||||
|
||||
if self.details.header {
|
||||
|
@ -199,11 +214,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();
|
||||
|
@ -228,17 +242,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 {
|
||||
|
@ -247,6 +263,7 @@ impl<'a> Render<'a> {
|
|||
let cell = grid::Cell {
|
||||
contents: ANSIStrings(&column[row].contents).to_string(),
|
||||
width: *column[row].width,
|
||||
alignment: grid::Alignment::Left,
|
||||
};
|
||||
|
||||
grid.add(cell);
|
||||
|
@ -260,6 +277,7 @@ impl<'a> Render<'a> {
|
|||
let cell = grid::Cell {
|
||||
contents: ANSIStrings(&cell.contents).to_string(),
|
||||
width: *cell.width,
|
||||
alignment: grid::Alignment::Left,
|
||||
};
|
||||
|
||||
grid.add(cell);
|
||||
|
@ -274,14 +292,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 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
use ansi_term::Style;
|
||||
use fs::File;
|
||||
use info::filetype::FileExtensions;
|
||||
use output::file_name::FileStyle;
|
||||
|
||||
use crate::fs::File;
|
||||
use crate::info::filetype::FileExtensions;
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
pub trait FileIcon {
|
||||
fn icon_file(&self, file: &File) -> Option<char>;
|
||||
fn icon_file(&self, file: &File<'_>) -> Option<char>;
|
||||
}
|
||||
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Icons {
|
||||
Audio,
|
||||
Image,
|
||||
|
@ -14,107 +19,356 @@ 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 painted = style.exts
|
||||
.colour_file(&file)
|
||||
.map_or(file_icon.to_string(), |c| {
|
||||
// Remove underline from icon
|
||||
if c.is_underline {
|
||||
match c.foreground {
|
||||
Some(color) => Style::from(color).paint(file_icon).to_string(),
|
||||
None => Style::default().paint(file_icon).to_string(),
|
||||
}
|
||||
} else {
|
||||
c.paint(file_icon).to_string()
|
||||
}
|
||||
});
|
||||
format!("{} ", painted)
|
||||
|
||||
/// Converts the style used to paint a file name into the style that should be
|
||||
/// used to paint an icon.
|
||||
///
|
||||
/// - The background colour should be preferred to the foreground colour, as
|
||||
/// if one is set, it’s the more “obvious” colour choice.
|
||||
/// - If neither is set, just use the default style.
|
||||
/// - Attributes such as bold or underline should not be used to paint the
|
||||
/// icon, as they can make it look weird.
|
||||
pub fn iconify_style(style: Style) -> Style {
|
||||
style.background.or(style.foreground)
|
||||
.map(Style::from)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn icon(file: &File) -> char {
|
||||
|
||||
|
||||
lazy_static! {
|
||||
static ref MAP_BY_NAME: HashMap<&'static str, char> = {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(".Trash", '\u{f1f8}'); //
|
||||
m.insert(".atom", '\u{e764}'); //
|
||||
m.insert(".bashprofile", '\u{e615}'); //
|
||||
m.insert(".bashrc", '\u{f489}'); //
|
||||
m.insert(".git", '\u{f1d3}'); //
|
||||
m.insert(".gitattributes", '\u{f1d3}'); //
|
||||
m.insert(".gitconfig", '\u{f1d3}'); //
|
||||
m.insert(".github", '\u{f408}'); //
|
||||
m.insert(".gitignore", '\u{f1d3}'); //
|
||||
m.insert(".gitmodules", '\u{f1d3}'); //
|
||||
m.insert(".rvm", '\u{e21e}'); //
|
||||
m.insert(".vimrc", '\u{e62b}'); //
|
||||
m.insert(".vscode", '\u{e70c}'); //
|
||||
m.insert(".zshrc", '\u{f489}'); //
|
||||
m.insert("Cargo.lock", '\u{e7a8}'); //
|
||||
m.insert("bin", '\u{e5fc}'); //
|
||||
m.insert("config", '\u{e5fc}'); //
|
||||
m.insert("docker-compose.yml", '\u{f308}'); //
|
||||
m.insert("Dockerfile", '\u{f308}'); //
|
||||
m.insert("ds_store", '\u{f179}'); //
|
||||
m.insert("gitignore_global", '\u{f1d3}'); //
|
||||
m.insert("go.mod", '\u{e626}'); //
|
||||
m.insert("go.sum", '\u{e626}'); //
|
||||
m.insert("gradle", '\u{e256}'); //
|
||||
m.insert("gruntfile.coffee", '\u{e611}'); //
|
||||
m.insert("gruntfile.js", '\u{e611}'); //
|
||||
m.insert("gruntfile.ls", '\u{e611}'); //
|
||||
m.insert("gulpfile.coffee", '\u{e610}'); //
|
||||
m.insert("gulpfile.js", '\u{e610}'); //
|
||||
m.insert("gulpfile.ls", '\u{e610}'); //
|
||||
m.insert("hidden", '\u{f023}'); //
|
||||
m.insert("include", '\u{e5fc}'); //
|
||||
m.insert("lib", '\u{f121}'); //
|
||||
m.insert("localized", '\u{f179}'); //
|
||||
m.insert("Makefile", '\u{f489}'); //
|
||||
m.insert("node_modules", '\u{e718}'); //
|
||||
m.insert("npmignore", '\u{e71e}'); //
|
||||
m.insert("PKGBUILD", '\u{f303}'); //
|
||||
m.insert("rubydoc", '\u{e73b}'); //
|
||||
m.insert("yarn.lock", '\u{e718}'); //
|
||||
|
||||
m
|
||||
};
|
||||
}
|
||||
|
||||
pub fn icon_for_file(file: &File<'_>) -> char {
|
||||
let extensions = Box::new(FileExtensions);
|
||||
if file.is_directory() { '\u{f115}' }
|
||||
else if let Some(icon) = extensions.icon_file(file) { icon }
|
||||
else {
|
||||
if let Some(ext) = file.ext.as_ref() {
|
||||
match ext.as_str() {
|
||||
"ai" => '\u{e7b4}',
|
||||
"android" => '\u{e70e}',
|
||||
"apple" => '\u{f179}',
|
||||
"avro" => '\u{e60b}',
|
||||
"c" => '\u{e61e}',
|
||||
"clj" => '\u{e768}',
|
||||
"coffee" => '\u{f0f4}',
|
||||
"conf" => '\u{e615}',
|
||||
"cpp" => '\u{e61d}',
|
||||
"css" => '\u{e749}',
|
||||
"d" => '\u{e7af}',
|
||||
"dart" => '\u{e798}',
|
||||
"db" => '\u{f1c0}',
|
||||
"diff" => '\u{f440}',
|
||||
"doc" => '\u{f1c2}',
|
||||
"ebook" => '\u{e28b}',
|
||||
"env" => '\u{f462}',
|
||||
"epub" => '\u{e28a}',
|
||||
"erl" => '\u{e7b1}',
|
||||
"font" => '\u{f031}',
|
||||
"gform" => '\u{f298}',
|
||||
"git" => '\u{f1d3}',
|
||||
"go" => '\u{e626}',
|
||||
"hs" => '\u{e777}',
|
||||
"html" => '\u{f13b}',
|
||||
"iml" => '\u{e7b5}',
|
||||
"java" => '\u{e204}',
|
||||
"js" => '\u{e74e}',
|
||||
"json" => '\u{e60b}',
|
||||
"jsx" => '\u{e7ba}',
|
||||
"less" => '\u{e758}',
|
||||
"log" => '\u{f18d}',
|
||||
"lua" => '\u{e620}',
|
||||
"md" => '\u{f48a}',
|
||||
"mustache" => '\u{e60f}',
|
||||
"npmignore" => '\u{e71e}',
|
||||
"pdf" => '\u{f1c1}',
|
||||
"php" => '\u{e73d}',
|
||||
"pl" => '\u{e769}',
|
||||
"ppt" => '\u{f1c4}',
|
||||
"psd" => '\u{e7b8}',
|
||||
"py" => '\u{e606}',
|
||||
"r" => '\u{f25d}',
|
||||
"rb" => '\u{e21e}',
|
||||
"rdb" => '\u{e76d}',
|
||||
"rs" => '\u{e7a8}',
|
||||
"rss" => '\u{f09e}',
|
||||
"rubydoc" => '\u{e73b}',
|
||||
"sass" => '\u{e603}',
|
||||
"scala" => '\u{e737}',
|
||||
"shell" => '\u{f489}',
|
||||
"sqlite3" => '\u{e7c4}',
|
||||
"styl" => '\u{e600}',
|
||||
"tex" => '\u{e600}',
|
||||
"ts" => '\u{e628}',
|
||||
"twig" => '\u{e61c}',
|
||||
"txt" => '\u{f15c}',
|
||||
"video" => '\u{f03d}',
|
||||
"vim" => '\u{e62b}',
|
||||
"xls" => '\u{f1c3}',
|
||||
"xml" => '\u{e619}',
|
||||
"yml" => '\u{f481}',
|
||||
"zip" => '\u{f410}',
|
||||
_ => '\u{f15b}'
|
||||
}
|
||||
} else {
|
||||
'\u{f15b}'
|
||||
|
||||
if let Some(icon) = MAP_BY_NAME.get(file.name.as_str()) { *icon }
|
||||
else if file.points_to_directory() {
|
||||
match file.name.as_str() {
|
||||
"bin" => '\u{e5fc}', //
|
||||
".git" => '\u{f1d3}', //
|
||||
".idea" => '\u{e7b5}', //
|
||||
_ => '\u{f115}' //
|
||||
}
|
||||
}
|
||||
else if let Some(icon) = extensions.icon_file(file) { icon }
|
||||
else if let Some(ext) = file.ext.as_ref() {
|
||||
match ext.as_str() {
|
||||
"ai" => '\u{e7b4}', //
|
||||
"android" => '\u{e70e}', //
|
||||
"apk" => '\u{e70e}', //
|
||||
"apple" => '\u{f179}', //
|
||||
"avi" => '\u{f03d}', //
|
||||
"avif" => '\u{f1c5}', //
|
||||
"avro" => '\u{e60b}', //
|
||||
"awk" => '\u{f489}', //
|
||||
"bash" => '\u{f489}', //
|
||||
"bash_history" => '\u{f489}', //
|
||||
"bash_profile" => '\u{f489}', //
|
||||
"bashrc" => '\u{f489}', //
|
||||
"bat" => '\u{f17a}', //
|
||||
"bats" => '\u{f489}', //
|
||||
"bmp" => '\u{f1c5}', //
|
||||
"bz" => '\u{f410}', //
|
||||
"bz2" => '\u{f410}', //
|
||||
"c" => '\u{e61e}', //
|
||||
"c++" => '\u{e61d}', //
|
||||
"cab" => '\u{e70f}', //
|
||||
"cc" => '\u{e61d}', //
|
||||
"cfg" => '\u{e615}', //
|
||||
"class" => '\u{e256}', //
|
||||
"clj" => '\u{e768}', //
|
||||
"cljs" => '\u{e76a}', //
|
||||
"cls" => '\u{f034}', //
|
||||
"cmd" => '\u{e70f}', //
|
||||
"coffee" => '\u{f0f4}', //
|
||||
"conf" => '\u{e615}', //
|
||||
"cp" => '\u{e61d}', //
|
||||
"cpio" => '\u{f410}', //
|
||||
"cpp" => '\u{e61d}', //
|
||||
"cs" => '\u{f031b}', //
|
||||
"csh" => '\u{f489}', //
|
||||
"cshtml" => '\u{f1fa}', //
|
||||
"csproj" => '\u{f031b}', //
|
||||
"css" => '\u{e749}', //
|
||||
"csv" => '\u{f1c3}', //
|
||||
"csx" => '\u{f031b}', //
|
||||
"cxx" => '\u{e61d}', //
|
||||
"d" => '\u{e7af}', //
|
||||
"dart" => '\u{e798}', //
|
||||
"db" => '\u{f1c0}', //
|
||||
"deb" => '\u{e77d}', //
|
||||
"diff" => '\u{f440}', //
|
||||
"djvu" => '\u{f02d}', //
|
||||
"dll" => '\u{e70f}', //
|
||||
"doc" => '\u{f1c2}', //
|
||||
"docx" => '\u{f1c2}', //
|
||||
"ds_store" => '\u{f179}', //
|
||||
"DS_store" => '\u{f179}', //
|
||||
"dump" => '\u{f1c0}', //
|
||||
"ebook" => '\u{e28b}', //
|
||||
"ebuild" => '\u{f30d}', //
|
||||
"editorconfig" => '\u{e615}', //
|
||||
"ejs" => '\u{e618}', //
|
||||
"elm" => '\u{e62c}', //
|
||||
"env" => '\u{f462}', //
|
||||
"eot" => '\u{f031}', //
|
||||
"epub" => '\u{e28a}', //
|
||||
"erb" => '\u{e73b}', //
|
||||
"erl" => '\u{e7b1}', //
|
||||
"ex" => '\u{e62d}', //
|
||||
"exe" => '\u{f17a}', //
|
||||
"exs" => '\u{e62d}', //
|
||||
"fish" => '\u{f489}', //
|
||||
"flac" => '\u{f001}', //
|
||||
"flv" => '\u{f03d}', //
|
||||
"font" => '\u{f031}', //
|
||||
"fs" => '\u{e7a7}', //
|
||||
"fsi" => '\u{e7a7}', //
|
||||
"fsx" => '\u{e7a7}', //
|
||||
"gdoc" => '\u{f1c2}', //
|
||||
"gem" => '\u{e21e}', //
|
||||
"gemfile" => '\u{e21e}', //
|
||||
"gemspec" => '\u{e21e}', //
|
||||
"gform" => '\u{f298}', //
|
||||
"gif" => '\u{f1c5}', //
|
||||
"git" => '\u{f1d3}', //
|
||||
"gitattributes" => '\u{f1d3}', //
|
||||
"gitignore" => '\u{f1d3}', //
|
||||
"gitmodules" => '\u{f1d3}', //
|
||||
"go" => '\u{e626}', //
|
||||
"gradle" => '\u{e256}', //
|
||||
"groovy" => '\u{e775}', //
|
||||
"gsheet" => '\u{f1c3}', //
|
||||
"gslides" => '\u{f1c4}', //
|
||||
"guardfile" => '\u{e21e}', //
|
||||
"gz" => '\u{f410}', //
|
||||
"h" => '\u{f0fd}', //
|
||||
"hbs" => '\u{e60f}', //
|
||||
"hpp" => '\u{f0fd}', //
|
||||
"hs" => '\u{e777}', //
|
||||
"htm" => '\u{f13b}', //
|
||||
"html" => '\u{f13b}', //
|
||||
"hxx" => '\u{f0fd}', //
|
||||
"ico" => '\u{f1c5}', //
|
||||
"image" => '\u{f1c5}', //
|
||||
"img" => '\u{e271}', //
|
||||
"iml" => '\u{e7b5}', //
|
||||
"ini" => '\u{f17a}', //
|
||||
"ipynb" => '\u{e678}', //
|
||||
"iso" => '\u{e271}', //
|
||||
"j2c" => '\u{f1c5}', //
|
||||
"j2k" => '\u{f1c5}', //
|
||||
"jad" => '\u{e256}', //
|
||||
"jar" => '\u{e256}', //
|
||||
"java" => '\u{e256}', //
|
||||
"jfi" => '\u{f1c5}', //
|
||||
"jfif" => '\u{f1c5}', //
|
||||
"jif" => '\u{f1c5}', //
|
||||
"jl" => '\u{e624}', //
|
||||
"jmd" => '\u{f48a}', //
|
||||
"jp2" => '\u{f1c5}', //
|
||||
"jpe" => '\u{f1c5}', //
|
||||
"jpeg" => '\u{f1c5}', //
|
||||
"jpg" => '\u{f1c5}', //
|
||||
"jpx" => '\u{f1c5}', //
|
||||
"js" => '\u{e74e}', //
|
||||
"json" => '\u{e60b}', //
|
||||
"jsx" => '\u{e7ba}', //
|
||||
"jxl" => '\u{f1c5}', //
|
||||
"ksh" => '\u{f489}', //
|
||||
"latex" => '\u{f034}', //
|
||||
"less" => '\u{e758}', //
|
||||
"lhs" => '\u{e777}', //
|
||||
"license" => '\u{f0219}', //
|
||||
"localized" => '\u{f179}', //
|
||||
"lock" => '\u{f023}', //
|
||||
"log" => '\u{f18d}', //
|
||||
"lua" => '\u{e620}', //
|
||||
"lz" => '\u{f410}', //
|
||||
"lz4" => '\u{f410}', //
|
||||
"lzh" => '\u{f410}', //
|
||||
"lzma" => '\u{f410}', //
|
||||
"lzo" => '\u{f410}', //
|
||||
"m" => '\u{e61e}', //
|
||||
"mm" => '\u{e61d}', //
|
||||
"m4a" => '\u{f001}', //
|
||||
"markdown" => '\u{f48a}', //
|
||||
"md" => '\u{f48a}', //
|
||||
"mjs" => '\u{e74e}', //
|
||||
"mk" => '\u{f489}', //
|
||||
"mkd" => '\u{f48a}', //
|
||||
"mkv" => '\u{f03d}', //
|
||||
"mobi" => '\u{e28b}', //
|
||||
"mov" => '\u{f03d}', //
|
||||
"mp3" => '\u{f001}', //
|
||||
"mp4" => '\u{f03d}', //
|
||||
"msi" => '\u{e70f}', //
|
||||
"mustache" => '\u{e60f}', //
|
||||
"nix" => '\u{f313}', //
|
||||
"node" => '\u{f0399}', //
|
||||
"npmignore" => '\u{e71e}', //
|
||||
"odp" => '\u{f1c4}', //
|
||||
"ods" => '\u{f1c3}', //
|
||||
"odt" => '\u{f1c2}', //
|
||||
"ogg" => '\u{f001}', //
|
||||
"ogv" => '\u{f03d}', //
|
||||
"otf" => '\u{f031}', //
|
||||
"part" => '\u{f43a}', //
|
||||
"patch" => '\u{f440}', //
|
||||
"pdf" => '\u{f1c1}', //
|
||||
"php" => '\u{e73d}', //
|
||||
"pl" => '\u{e769}', //
|
||||
"plx" => '\u{e769}', //
|
||||
"pm" => '\u{e769}', //
|
||||
"png" => '\u{f1c5}', //
|
||||
"pod" => '\u{e769}', //
|
||||
"ppt" => '\u{f1c4}', //
|
||||
"pptx" => '\u{f1c4}', //
|
||||
"procfile" => '\u{e21e}', //
|
||||
"properties" => '\u{e60b}', //
|
||||
"ps1" => '\u{f489}', //
|
||||
"psd" => '\u{e7b8}', //
|
||||
"pxm" => '\u{f1c5}', //
|
||||
"py" => '\u{e606}', //
|
||||
"pyc" => '\u{e606}', //
|
||||
"r" => '\u{f25d}', //
|
||||
"rakefile" => '\u{e21e}', //
|
||||
"rar" => '\u{f410}', //
|
||||
"razor" => '\u{f1fa}', //
|
||||
"rb" => '\u{e21e}', //
|
||||
"rdata" => '\u{f25d}', //
|
||||
"rdb" => '\u{e76d}', //
|
||||
"rdoc" => '\u{f48a}', //
|
||||
"rds" => '\u{f25d}', //
|
||||
"readme" => '\u{f48a}', //
|
||||
"rlib" => '\u{e7a8}', //
|
||||
"rmd" => '\u{f48a}', //
|
||||
"rpm" => '\u{e7bb}', //
|
||||
"rs" => '\u{e7a8}', //
|
||||
"rspec" => '\u{e21e}', //
|
||||
"rspec_parallel"=> '\u{e21e}', //
|
||||
"rspec_status" => '\u{e21e}', //
|
||||
"rss" => '\u{f09e}', //
|
||||
"rtf" => '\u{f0219}', //
|
||||
"ru" => '\u{e21e}', //
|
||||
"rubydoc" => '\u{e73b}', //
|
||||
"sass" => '\u{e603}', //
|
||||
"scala" => '\u{e737}', //
|
||||
"scss" => '\u{e749}', //
|
||||
"sh" => '\u{f489}', //
|
||||
"shell" => '\u{f489}', //
|
||||
"slim" => '\u{e73b}', //
|
||||
"sln" => '\u{e70c}', //
|
||||
"so" => '\u{f17c}', //
|
||||
"sql" => '\u{f1c0}', //
|
||||
"sqlite3" => '\u{e7c4}', //
|
||||
"sty" => '\u{f034}', //
|
||||
"styl" => '\u{e600}', //
|
||||
"stylus" => '\u{e600}', //
|
||||
"svg" => '\u{f1c5}', //
|
||||
"swift" => '\u{e755}', //
|
||||
"t" => '\u{e769}', //
|
||||
"tar" => '\u{f410}', //
|
||||
"taz" => '\u{f410}', //
|
||||
"tbz" => '\u{f410}', //
|
||||
"tbz2" => '\u{f410}', //
|
||||
"tex" => '\u{f034}', //
|
||||
"tgz" => '\u{f410}', //
|
||||
"tiff" => '\u{f1c5}', //
|
||||
"tlz" => '\u{f410}', //
|
||||
"toml" => '\u{e615}', //
|
||||
"torrent" => '\u{e275}', //
|
||||
"ts" => '\u{e628}', //
|
||||
"tsv" => '\u{f1c3}', //
|
||||
"tsx" => '\u{e7ba}', //
|
||||
"ttf" => '\u{f031}', //
|
||||
"twig" => '\u{e61c}', //
|
||||
"txt" => '\u{f15c}', //
|
||||
"txz" => '\u{f410}', //
|
||||
"tz" => '\u{f410}', //
|
||||
"tzo" => '\u{f410}', //
|
||||
"video" => '\u{f03d}', //
|
||||
"vim" => '\u{e62b}', //
|
||||
"vue" => '\u{f0844}', //
|
||||
"war" => '\u{e256}', //
|
||||
"wav" => '\u{f001}', //
|
||||
"webm" => '\u{f03d}', //
|
||||
"webp" => '\u{f1c5}', //
|
||||
"windows" => '\u{f17a}', //
|
||||
"woff" => '\u{f031}', //
|
||||
"woff2" => '\u{f031}', //
|
||||
"xhtml" => '\u{f13b}', //
|
||||
"xls" => '\u{f1c3}', //
|
||||
"xlsx" => '\u{f1c3}', //
|
||||
"xml" => '\u{f05c0}', //
|
||||
"xul" => '\u{f05c0}', //
|
||||
"xz" => '\u{f410}', //
|
||||
"yaml" => '\u{f481}', //
|
||||
"yml" => '\u{f481}', //
|
||||
"zip" => '\u{f410}', //
|
||||
"zsh" => '\u{f489}', //
|
||||
"zsh-theme" => '\u{f489}', //
|
||||
"zshrc" => '\u{f489}', //
|
||||
"zst" => '\u{f410}', //
|
||||
_ => '\u{f15b}' //
|
||||
}
|
||||
}
|
||||
else {
|
||||
'\u{f016}'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1,37 @@
|
|||
use std::io::{Write, Result as IOResult};
|
||||
use std::io::{self, Write};
|
||||
|
||||
use ansi_term::{ANSIStrings, ANSIGenericString};
|
||||
use ansi_term::ANSIStrings;
|
||||
|
||||
use fs::File;
|
||||
use output::file_name::{FileName, FileStyle};
|
||||
use style::Colours;
|
||||
use output::icons::painted_icon;
|
||||
use output::cell::TextCell;
|
||||
use crate::fs::File;
|
||||
use crate::fs::filter::FileFilter;
|
||||
use crate::output::cell::TextCellContents;
|
||||
use crate::output::file_name::{Options as FileStyle};
|
||||
use crate::theme::Theme;
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct Options {
|
||||
pub icons: bool
|
||||
}
|
||||
|
||||
/// The lines view literally just displays each file, line-by-line.
|
||||
pub struct Render<'a> {
|
||||
pub files: Vec<File<'a>>,
|
||||
pub colours: &'a Colours,
|
||||
pub style: &'a FileStyle,
|
||||
pub opts: &'a Options,
|
||||
pub theme: &'a Theme,
|
||||
pub file_style: &'a FileStyle,
|
||||
pub filter: &'a FileFilter,
|
||||
}
|
||||
|
||||
impl<'a> Render<'a> {
|
||||
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
|
||||
pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
|
||||
self.filter.sort_files(&mut self.files);
|
||||
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);
|
||||
cell.push(ANSIGenericString::from(icon), 2);
|
||||
cell.append(name_cell.promote());
|
||||
writeln!(w, "{}", ANSIStrings(&cell))?;
|
||||
} else {
|
||||
writeln!(w, "{}", ANSIStrings(&name_cell))?;
|
||||
}
|
||||
let name_cell = self.render_file(file);
|
||||
writeln!(w, "{}", ANSIStrings(&name_cell))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_file<'f>(&self, file: &'f File<'a>) -> FileName<'f, 'a, Colours> {
|
||||
self.style.for_file(file, self.colours).with_link_paths()
|
||||
fn render_file<'f>(&self, file: &'f File<'a>) -> TextCellContents {
|
||||
self.file_style
|
||||
.for_file(file, self.theme)
|
||||
.with_link_paths()
|
||||
.paint()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
use output::file_name::FileStyle;
|
||||
use style::Colours;
|
||||
|
||||
pub use self::cell::{TextCell, TextCellContents, DisplayWidth};
|
||||
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;
|
||||
|
@ -23,17 +20,42 @@ mod tree;
|
|||
#[derive(Debug)]
|
||||
pub struct View {
|
||||
pub mode: Mode,
|
||||
pub colours: Colours,
|
||||
pub style: FileStyle,
|
||||
pub width: TerminalWidth,
|
||||
pub file_style: file_name::Options,
|
||||
}
|
||||
|
||||
|
||||
/// The **mode** is the “type” of output.
|
||||
#[derive(Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Mode {
|
||||
Grid(grid::Options),
|
||||
Details(details::Options),
|
||||
GridDetails(grid_details::Options),
|
||||
Lines(lines::Options),
|
||||
Lines,
|
||||
}
|
||||
|
||||
|
||||
/// The width of the terminal requested by the user.
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum TerminalWidth {
|
||||
|
||||
/// The user requested this specific number of columns.
|
||||
Set(usize),
|
||||
|
||||
/// Look up the terminal size at runtime.
|
||||
Automatic,
|
||||
}
|
||||
|
||||
impl TerminalWidth {
|
||||
pub fn actual_terminal_width(self) -> Option<usize> {
|
||||
// All of stdin, stdout, and stderr could not be connected to a
|
||||
// terminal, but we’re only interested in stdout because it’s
|
||||
// where the output goes.
|
||||
|
||||
match self {
|
||||
Self::Set(width) => Some(width),
|
||||
Self::Automatic => terminal_size::terminal_size().map(|(w, _)| w.0.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use ansi_term::Style;
|
||||
|
||||
use output::cell::TextCell;
|
||||
use fs::fields as f;
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,8 @@ pub mod test {
|
|||
use ansi_term::Colour::*;
|
||||
|
||||
use super::Colours;
|
||||
use output::cell::TextCell;
|
||||
use fs::fields as f;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::fs::fields as f;
|
||||
|
||||
|
||||
struct TestColours;
|
||||
|
@ -43,7 +43,7 @@ pub mod test {
|
|||
let blox = f::Blocks::None;
|
||||
let expected = TextCell::blank(Green.italic());
|
||||
|
||||
assert_eq!(expected, blox.render(&TestColours).into());
|
||||
assert_eq!(expected, blox.render(&TestColours));
|
||||
}
|
||||
|
||||
|
||||
|
@ -52,6 +52,6 @@ pub mod test {
|
|||
let blox = f::Blocks::Some(3005);
|
||||
let expected = TextCell::paint_str(Red.blink(), "3005");
|
||||
|
||||
assert_eq!(expected, blox.render(&TestColours).into());
|
||||
assert_eq!(expected, blox.render(&TestColours));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
use ansi_term::{ANSIString, Style};
|
||||
|
||||
use fs::fields as f;
|
||||
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("?"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use ansi_term::{ANSIString, Style};
|
||||
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use fs::fields as f;
|
||||
use crate::output::cell::{TextCell, DisplayWidth};
|
||||
use crate::fs::fields as f;
|
||||
|
||||
|
||||
impl f::Git {
|
||||
pub fn render(&self, colours: &Colours) -> TextCell {
|
||||
pub fn render(self, colours: &dyn Colours) -> TextCell {
|
||||
TextCell {
|
||||
width: DisplayWidth::from(2),
|
||||
contents: vec![
|
||||
|
@ -18,15 +18,16 @@ impl f::Git {
|
|||
|
||||
|
||||
impl f::GitStatus {
|
||||
fn render(&self, colours: &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"),
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,20 +35,22 @@ impl f::GitStatus {
|
|||
|
||||
pub trait Colours {
|
||||
fn not_modified(&self) -> Style;
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
fn new(&self) -> Style;
|
||||
fn modified(&self) -> Style;
|
||||
fn deleted(&self) -> Style;
|
||||
fn renamed(&self) -> Style;
|
||||
fn type_change(&self) -> Style;
|
||||
fn ignored(&self) -> Style;
|
||||
fn conflicted(&self) -> Style;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use super::Colours;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use fs::fields as f;
|
||||
use crate::output::cell::{TextCell, DisplayWidth};
|
||||
use crate::fs::fields as f;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
use ansi_term::Style;
|
||||
|
@ -63,6 +66,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(97).normal() }
|
||||
}
|
||||
|
||||
|
||||
|
@ -81,7 +85,7 @@ pub mod test {
|
|||
].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&TestColours).into())
|
||||
assert_eq!(expected, stati.render(&TestColours))
|
||||
}
|
||||
|
||||
|
||||
|
@ -100,6 +104,6 @@ pub mod test {
|
|||
].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&TestColours).into())
|
||||
assert_eq!(expected, stati.render(&TestColours))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,38 @@
|
|||
use ansi_term::Style;
|
||||
use users::{Users, Groups};
|
||||
|
||||
use fs::fields as f;
|
||||
use output::cell::TextCell;
|
||||
use crate::fs::fields as f;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::output::table::UserFormat;
|
||||
|
||||
|
||||
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, format: UserFormat) -> 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();
|
||||
}
|
||||
}
|
||||
|
||||
TextCell::paint(style, group.name().to_string_lossy().into())
|
||||
let group_name = match format {
|
||||
UserFormat::Name => group.name().to_string_lossy().into(),
|
||||
UserFormat::Numeric => group.gid().to_string(),
|
||||
};
|
||||
|
||||
TextCell::paint(style, group_name)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,8 +47,9 @@ pub trait Colours {
|
|||
#[allow(unused_results)]
|
||||
pub mod test {
|
||||
use super::Colours;
|
||||
use fs::fields as f;
|
||||
use output::cell::TextCell;
|
||||
use crate::fs::fields as f;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::output::table::UserFormat;
|
||||
|
||||
use users::{User, Group};
|
||||
use users::mock::MockUsers;
|
||||
|
@ -64,16 +73,21 @@ pub mod test {
|
|||
|
||||
let group = f::Group(100);
|
||||
let expected = TextCell::paint_str(Fixed(81).normal(), "folk");
|
||||
assert_eq!(expected, group.render(&TestColours, &users))
|
||||
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name));
|
||||
|
||||
let expected = TextCell::paint_str(Fixed(81).normal(), "100");
|
||||
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Numeric));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn unnamed() {
|
||||
let users = MockUsers::with_current_uid(1000);
|
||||
|
||||
let group = f::Group(100);
|
||||
let expected = TextCell::paint_str(Fixed(81).normal(), "100");
|
||||
assert_eq!(expected, group.render(&TestColours, &users));
|
||||
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name));
|
||||
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Numeric));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -84,7 +98,7 @@ pub mod test {
|
|||
|
||||
let group = f::Group(100);
|
||||
let expected = TextCell::paint_str(Fixed(80).normal(), "folk");
|
||||
assert_eq!(expected, group.render(&TestColours, &users))
|
||||
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -97,13 +111,13 @@ pub mod test {
|
|||
|
||||
let group = f::Group(100);
|
||||
let expected = TextCell::paint_str(Fixed(80).normal(), "folk");
|
||||
assert_eq!(expected, group.render(&TestColours, &users))
|
||||
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow() {
|
||||
let group = f::Group(2_147_483_648);
|
||||
let expected = TextCell::paint_str(Fixed(81).normal(), "2147483648");
|
||||
assert_eq!(expected, group.render(&TestColours, &MockUsers::with_current_uid(0)));
|
||||
assert_eq!(expected, group.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use ansi_term::Style;
|
||||
|
||||
use output::cell::TextCell;
|
||||
use fs::fields as f;
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@ -13,16 +13,16 @@ impl f::Inode {
|
|||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use output::cell::TextCell;
|
||||
use fs::fields as f;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::fs::fields as f;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn blocklessness() {
|
||||
let io = f::Inode(1414213);
|
||||
let io = f::Inode(1_414_213);
|
||||
let expected = TextCell::paint_str(Cyan.underline(), "1414213");
|
||||
assert_eq!(expected, io.render(Cyan.underline()).into());
|
||||
assert_eq!(expected, io.render(Cyan.underline()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use ansi_term::Style;
|
||||
use locale::Numeric as NumericLocale;
|
||||
|
||||
use output::cell::TextCell;
|
||||
use fs::fields as f;
|
||||
use crate::fs::fields as f;
|
||||
use crate::output::cell::TextCell;
|
||||
|
||||
|
||||
impl f::Links {
|
||||
|
@ -24,8 +24,8 @@ pub trait Colours {
|
|||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use super::Colours;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use fs::fields as f;
|
||||
use crate::output::cell::{TextCell, DisplayWidth};
|
||||
use crate::fs::fields as f;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
use ansi_term::Style;
|
||||
|
@ -52,7 +52,7 @@ pub mod test {
|
|||
contents: vec![ Blue.paint("1") ].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()).into());
|
||||
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -67,7 +67,7 @@ pub mod test {
|
|||
contents: vec![ Blue.paint("3,005") ].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()).into());
|
||||
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -82,6 +82,6 @@ pub mod test {
|
|||
contents: vec![ Blue.on(Red).paint("3,005") ].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()).into());
|
||||
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ pub use self::filetype::Colours as FiletypeColours;
|
|||
mod git;
|
||||
pub use self::git::Colours as GitColours;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod groups;
|
||||
#[cfg(unix)]
|
||||
pub use self::groups::Colours as GroupColours;
|
||||
|
||||
mod inode;
|
||||
|
@ -26,5 +28,10 @@ mod times;
|
|||
pub use self::times::Render as TimeRender;
|
||||
// times does too
|
||||
|
||||
#[cfg(unix)]
|
||||
mod users;
|
||||
#[cfg(unix)]
|
||||
pub use self::users::Colours as UserColours;
|
||||
|
||||
mod octal;
|
||||
// octal uses just one colour
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
use ansi_term::Style;
|
||||
|
||||
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 {
|
||||
u8::from(r) * 4 + u8::from(w) * 2 + u8::from(x)
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
TextCell::paint(style, format!("{}{}{}{}", octal_sticky, octal_owner, octal_group, octal_other))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::fs::fields as f;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn normal_folder() {
|
||||
let bits = f::Permissions {
|
||||
user_read: true, user_write: true, user_execute: true, setuid: false,
|
||||
group_read: true, group_write: false, group_execute: true, setgid: false,
|
||||
other_read: true, other_write: false, other_execute: true, sticky: false,
|
||||
};
|
||||
|
||||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "0755");
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_file() {
|
||||
let bits = f::Permissions {
|
||||
user_read: true, user_write: true, user_execute: false, setuid: false,
|
||||
group_read: true, group_write: false, group_execute: false, setgid: false,
|
||||
other_read: true, other_write: false, other_execute: false, sticky: false,
|
||||
};
|
||||
|
||||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "0644");
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_file() {
|
||||
let bits = f::Permissions {
|
||||
user_read: true, user_write: true, user_execute: false, setuid: false,
|
||||
group_read: false, group_write: false, group_execute: false, setgid: false,
|
||||
other_read: false, other_write: false, other_execute: false, sticky: false,
|
||||
};
|
||||
|
||||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "0600");
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sticky1() {
|
||||
let bits = f::Permissions {
|
||||
user_read: true, user_write: true, user_execute: true, setuid: true,
|
||||
group_read: true, group_write: true, group_execute: true, setgid: false,
|
||||
other_read: true, other_write: true, other_execute: true, sticky: false,
|
||||
};
|
||||
|
||||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "4777");
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sticky2() {
|
||||
let bits = f::Permissions {
|
||||
user_read: true, user_write: true, user_execute: true, setuid: false,
|
||||
group_read: true, group_write: true, group_execute: true, setgid: true,
|
||||
other_read: true, other_write: true, other_execute: true, sticky: false,
|
||||
};
|
||||
|
||||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "2777");
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sticky3() {
|
||||
let bits = f::Permissions {
|
||||
user_read: true, user_write: true, user_execute: true, setuid: false,
|
||||
group_read: true, group_write: true, group_execute: true, setgid: false,
|
||||
other_read: true, other_write: true, other_execute: true, sticky: true,
|
||||
};
|
||||
|
||||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "1777");
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
use ansi_term::{ANSIString, Style};
|
||||
|
||||
use fs::fields as f;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use output::render::FiletypeColours;
|
||||
use crate::fs::fields as f;
|
||||
use crate::output::cell::{TextCell, DisplayWidth};
|
||||
use crate::output::render::FiletypeColours;
|
||||
|
||||
|
||||
impl f::PermissionsPlus {
|
||||
#[cfg(unix)]
|
||||
pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
|
||||
let mut chars = vec![ self.file_type.render(colours) ];
|
||||
chars.extend(self.permissions.render(colours, self.file_type.is_regular_file()));
|
||||
|
@ -22,26 +23,37 @@ impl f::PermissionsPlus {
|
|||
contents: chars.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
|
||||
let mut chars = vec![ self.attributes.render_type(colours) ];
|
||||
chars.extend(self.attributes.render(colours));
|
||||
|
||||
TextCell {
|
||||
width: DisplayWidth::from(chars.len()),
|
||||
contents: chars.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
]
|
||||
}
|
||||
|
@ -76,6 +88,33 @@ impl f::Permissions {
|
|||
}
|
||||
}
|
||||
|
||||
impl f::Attributes {
|
||||
pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> Vec<ANSIString<'static>> {
|
||||
let bit = |bit, chr: &'static str, style: Style| {
|
||||
if bit { style.paint(chr) }
|
||||
else { colours.dash().paint("-") }
|
||||
};
|
||||
|
||||
vec![
|
||||
bit(self.archive, "a", colours.normal()),
|
||||
bit(self.readonly, "r", colours.user_read()),
|
||||
bit(self.hidden, "h", colours.special_user_file()),
|
||||
bit(self.system, "s", colours.special_other()),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn render_type<C: Colours+FiletypeColours>(&self, colours: &C) -> ANSIString<'static> {
|
||||
if self.reparse_point {
|
||||
return colours.pipe().paint("l")
|
||||
}
|
||||
else if self.directory {
|
||||
return colours.directory().paint("d")
|
||||
}
|
||||
else {
|
||||
return colours.dash().paint("-")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Colours {
|
||||
fn dash(&self) -> Style;
|
||||
|
@ -104,8 +143,8 @@ pub trait Colours {
|
|||
#[allow(unused_results)]
|
||||
pub mod test {
|
||||
use super::Colours;
|
||||
use output::cell::TextCellContents;
|
||||
use fs::fields as f;
|
||||
use crate::output::cell::TextCellContents;
|
||||
use crate::fs::fields as f;
|
||||
|
||||
use ansi_term::Colour::*;
|
||||
use ansi_term::Style;
|
||||
|
|
|
@ -1,49 +1,58 @@
|
|||
use ansi_term::Style;
|
||||
use locale::Numeric as NumericLocale;
|
||||
use number_prefix::Prefix;
|
||||
|
||||
use fs::fields as f;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use output::table::SizeFormat;
|
||||
|
||||
use crate::fs::fields as f;
|
||||
use crate::output::cell::{TextCell, DisplayWidth};
|
||||
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) {
|
||||
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(size), string);
|
||||
},
|
||||
|
||||
return TextCell::paint(colours.size(prefix), string);
|
||||
}
|
||||
};
|
||||
|
||||
let (prefix, n) = match result {
|
||||
Standalone(b) => return TextCell::paint(colours.size(b as u64), b.to_string()),
|
||||
Prefixed(p, n) => (p, n)
|
||||
NumberPrefix::Standalone(b) => return TextCell::paint(colours.size(None), numerics.format_int(b)),
|
||||
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) };
|
||||
|
||||
// The numbers and symbols are guaranteed to be written in ASCII, so
|
||||
// we can skip the display width calculation.
|
||||
let width = DisplayWidth::from(number.len() + symbol.len());
|
||||
let number = if n < 10_f64 {
|
||||
numerics.format_float(n, 1)
|
||||
} else {
|
||||
numerics.format_int(n.round() as isize)
|
||||
};
|
||||
|
||||
TextCell {
|
||||
width,
|
||||
// symbol is guaranteed to be ASCII since unit prefixes are hardcoded.
|
||||
width: DisplayWidth::from(&*number) + symbol.len(),
|
||||
contents: vec![
|
||||
colours.size(size).paint(number),
|
||||
colours.unit().paint(symbol),
|
||||
colours.size(Some(prefix)).paint(number),
|
||||
colours.unit(Some(prefix)).paint(symbol),
|
||||
].into(),
|
||||
}
|
||||
}
|
||||
|
@ -51,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();
|
||||
|
||||
|
@ -68,8 +77,8 @@ impl f::DeviceIDs {
|
|||
|
||||
|
||||
pub trait Colours {
|
||||
fn size(&self, size: u64) -> Style;
|
||||
fn unit(&self) -> Style;
|
||||
fn size(&self, prefix: Option<Prefix>) -> Style;
|
||||
fn unit(&self, prefix: Option<Prefix>) -> Style;
|
||||
fn no_size(&self) -> Style;
|
||||
|
||||
fn major(&self) -> Style;
|
||||
|
@ -81,20 +90,21 @@ pub trait Colours {
|
|||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use super::Colours;
|
||||
use output::cell::{TextCell, DisplayWidth};
|
||||
use output::table::SizeFormat;
|
||||
use fs::fields as f;
|
||||
use crate::output::cell::{TextCell, DisplayWidth};
|
||||
use crate::output::table::SizeFormat;
|
||||
use crate::fs::fields as f;
|
||||
|
||||
use locale::Numeric as NumericLocale;
|
||||
use ansi_term::Colour::*;
|
||||
use ansi_term::Style;
|
||||
use number_prefix::Prefix;
|
||||
|
||||
|
||||
struct TestColours;
|
||||
|
||||
impl Colours for TestColours {
|
||||
fn size(&self, _size: u64) -> Style { Fixed(66).normal() }
|
||||
fn unit(&self) -> Style { Fixed(77).bold() }
|
||||
fn size(&self, _prefix: Option<Prefix>) -> Style { Fixed(66).normal() }
|
||||
fn unit(&self, _prefix: Option<Prefix>) -> Style { Fixed(77).bold() }
|
||||
fn no_size(&self) -> Style { Black.italic() }
|
||||
|
||||
fn major(&self) -> Style { Blue.on(Red) }
|
||||
|
@ -143,7 +153,7 @@ pub mod test {
|
|||
|
||||
#[test]
|
||||
fn file_bytes() {
|
||||
let directory = f::Size::Some(1048576);
|
||||
let directory = f::Size::Some(1_048_576);
|
||||
let expected = TextCell {
|
||||
width: DisplayWidth::from(9),
|
||||
contents: vec![
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
use std::time::SystemTime;
|
||||
|
||||
use datetime::TimeZone;
|
||||
use ansi_term::Style;
|
||||
|
||||
use output::cell::TextCell;
|
||||
use output::time::TimeFormat;
|
||||
use crate::output::cell::TextCell;
|
||||
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 std::time::Duration {
|
||||
fn render(self, style: Style,
|
||||
tz: &Option<TimeZone>,
|
||||
format: &TimeFormat) -> TextCell {
|
||||
|
||||
if let Some(ref tz) = *tz {
|
||||
let datestamp = format.format_zoned(self, tz);
|
||||
TextCell::paint(style, datestamp)
|
||||
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 {
|
||||
format.format_local(time)
|
||||
}
|
||||
}
|
||||
else {
|
||||
let datestamp = format.format_local(self);
|
||||
TextCell::paint(style, datestamp)
|
||||
}
|
||||
String::from("-")
|
||||
};
|
||||
|
||||
TextCell::paint(style, datestamp)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
use ansi_term::Style;
|
||||
use users::Users;
|
||||
|
||||
use fs::fields as f;
|
||||
use output::cell::TextCell;
|
||||
|
||||
use crate::fs::fields as f;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::output::table::UserFormat;
|
||||
|
||||
|
||||
impl f::User {
|
||||
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(),
|
||||
pub fn render<C: Colours, U: Users>(self, colours: &C, users: &U, format: UserFormat) -> TextCell {
|
||||
let user_name = match (format, users.get_user_by_uid(self.0)) {
|
||||
(_, None) => self.0.to_string(),
|
||||
(UserFormat::Numeric, _) => self.0.to_string(),
|
||||
(UserFormat::Name, Some(user)) => user.name().to_string_lossy().into(),
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -30,8 +31,9 @@ pub trait Colours {
|
|||
#[allow(unused_results)]
|
||||
pub mod test {
|
||||
use super::Colours;
|
||||
use fs::fields as f;
|
||||
use output::cell::TextCell;
|
||||
use crate::fs::fields as f;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::output::table::UserFormat;
|
||||
|
||||
use users::User;
|
||||
use users::mock::MockUsers;
|
||||
|
@ -54,7 +56,10 @@ pub mod test {
|
|||
|
||||
let user = f::User(1000);
|
||||
let expected = TextCell::paint_str(Red.bold(), "enoch");
|
||||
assert_eq!(expected, user.render(&TestColours, &users))
|
||||
assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
|
||||
|
||||
let expected = TextCell::paint_str(Red.bold(), "1000");
|
||||
assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Numeric));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -63,7 +68,8 @@ pub mod test {
|
|||
|
||||
let user = f::User(1000);
|
||||
let expected = TextCell::paint_str(Red.bold(), "1000");
|
||||
assert_eq!(expected, user.render(&TestColours, &users));
|
||||
assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
|
||||
assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Numeric));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -73,20 +79,20 @@ pub mod test {
|
|||
|
||||
let user = f::User(1000);
|
||||
let expected = TextCell::paint_str(Blue.underline(), "enoch");
|
||||
assert_eq!(expected, user.render(&TestColours, &users));
|
||||
assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_unnamed() {
|
||||
let user = f::User(1000);
|
||||
let expected = TextCell::paint_str(Blue.underline(), "1000");
|
||||
assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0)));
|
||||
assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow() {
|
||||
let user = f::User(2_147_483_648);
|
||||
let expected = TextCell::paint_str(Blue.underline(), "2147483648");
|
||||
assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0)));
|
||||
assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,37 @@
|
|||
use std::cmp::max;
|
||||
use std::fmt;
|
||||
use std::env;
|
||||
use std::ops::Deref;
|
||||
#[cfg(unix)]
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use datetime::TimeZone;
|
||||
use zoneinfo_compiled::{CompiledData, Result as TZResult};
|
||||
|
||||
use locale;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use log::*;
|
||||
#[cfg(unix)]
|
||||
use users::UsersCache;
|
||||
|
||||
use style::Colours;
|
||||
use output::cell::TextCell;
|
||||
use output::render::TimeRender;
|
||||
use output::time::TimeFormat;
|
||||
use fs::{File, fields as f};
|
||||
use fs::feature::git::GitCache;
|
||||
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::theme::Theme;
|
||||
|
||||
|
||||
/// Options for displaying a table.
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Options {
|
||||
pub env: Environment,
|
||||
pub size_format: SizeFormat,
|
||||
pub time_format: TimeFormat,
|
||||
pub extra_columns: Columns,
|
||||
}
|
||||
|
||||
// I had to make other types derive Debug,
|
||||
// and Mutex<UsersCache> is not that!
|
||||
impl fmt::Debug for Options {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
write!(f, "Table({:#?})", self.extra_columns)
|
||||
}
|
||||
pub user_format: UserFormat,
|
||||
pub columns: Columns,
|
||||
}
|
||||
|
||||
/// Extra columns to display in the table.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct Columns {
|
||||
|
||||
/// At least one of these timestamps will be shown.
|
||||
|
@ -47,6 +43,12 @@ pub struct Columns {
|
|||
pub blocks: bool,
|
||||
pub group: bool,
|
||||
pub git: bool,
|
||||
pub octal: bool,
|
||||
|
||||
// Defaults to true:
|
||||
pub permissions: bool,
|
||||
pub filesize: bool,
|
||||
pub user: bool,
|
||||
}
|
||||
|
||||
impl Columns {
|
||||
|
@ -54,24 +56,40 @@ impl Columns {
|
|||
let mut columns = Vec::with_capacity(4);
|
||||
|
||||
if self.inode {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Inode);
|
||||
}
|
||||
|
||||
columns.push(Column::Permissions);
|
||||
if self.octal {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Octal);
|
||||
}
|
||||
|
||||
if self.permissions {
|
||||
columns.push(Column::Permissions);
|
||||
}
|
||||
|
||||
if self.links {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::HardLinks);
|
||||
}
|
||||
|
||||
columns.push(Column::FileSize);
|
||||
if self.filesize {
|
||||
columns.push(Column::FileSize);
|
||||
}
|
||||
|
||||
if self.blocks {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Blocks);
|
||||
}
|
||||
|
||||
columns.push(Column::User);
|
||||
if self.user {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::User);
|
||||
}
|
||||
|
||||
if self.group {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Group);
|
||||
}
|
||||
|
||||
|
@ -91,7 +109,7 @@ impl Columns {
|
|||
columns.push(Column::Timestamp(TimeType::Accessed));
|
||||
}
|
||||
|
||||
if cfg!(feature="git") && self.git && actually_enable_git {
|
||||
if self.git && actually_enable_git {
|
||||
columns.push(Column::GitStatus);
|
||||
}
|
||||
|
||||
|
@ -101,60 +119,89 @@ impl Columns {
|
|||
|
||||
|
||||
/// A table contains these.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Column {
|
||||
Permissions,
|
||||
FileSize,
|
||||
Timestamp(TimeType),
|
||||
#[cfg(unix)]
|
||||
Blocks,
|
||||
#[cfg(unix)]
|
||||
User,
|
||||
#[cfg(unix)]
|
||||
Group,
|
||||
#[cfg(unix)]
|
||||
HardLinks,
|
||||
#[cfg(unix)]
|
||||
Inode,
|
||||
GitStatus,
|
||||
#[cfg(unix)]
|
||||
Octal,
|
||||
}
|
||||
|
||||
/// Each column can pick its own **Alignment**. Usually, numbers are
|
||||
/// 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.
|
||||
#[cfg(unix)]
|
||||
pub fn alignment(self) -> Alignment {
|
||||
match self {
|
||||
Self::FileSize |
|
||||
Self::HardLinks |
|
||||
Self::Inode |
|
||||
Self::Blocks |
|
||||
Self::GitStatus => Alignment::Right,
|
||||
_ => Alignment::Left,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn alignment(&self) -> Alignment {
|
||||
match *self {
|
||||
Column::FileSize
|
||||
| Column::HardLinks
|
||||
| Column::Inode
|
||||
| Column::Blocks
|
||||
| Column::GitStatus => Alignment::Right,
|
||||
_ => Alignment::Left,
|
||||
match self {
|
||||
Self::FileSize |
|
||||
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",
|
||||
pub fn header(self) -> &'static str {
|
||||
match self {
|
||||
#[cfg(unix)]
|
||||
Self::Permissions => "Permissions",
|
||||
#[cfg(windows)]
|
||||
Self::Permissions => "Mode",
|
||||
Self::FileSize => "Size",
|
||||
Self::Timestamp(t) => t.header(),
|
||||
#[cfg(unix)]
|
||||
Self::Blocks => "Blocks",
|
||||
#[cfg(unix)]
|
||||
Self::User => "User",
|
||||
#[cfg(unix)]
|
||||
Self::Group => "Group",
|
||||
#[cfg(unix)]
|
||||
Self::HardLinks => "Links",
|
||||
#[cfg(unix)]
|
||||
Self::Inode => "inode",
|
||||
Self::GitStatus => "Git",
|
||||
#[cfg(unix)]
|
||||
Self::Octal => "Octal",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Formatting options for file sizes.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum SizeFormat {
|
||||
|
||||
/// Format the file size using **decimal** prefixes, such as “kilo”,
|
||||
|
@ -169,17 +216,27 @@ pub enum SizeFormat {
|
|||
JustBytes,
|
||||
}
|
||||
|
||||
/// Formatting options for user and group.
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum UserFormat {
|
||||
/// The UID / GID
|
||||
Numeric,
|
||||
/// Show the name
|
||||
Name,
|
||||
}
|
||||
|
||||
impl Default for SizeFormat {
|
||||
fn default() -> SizeFormat {
|
||||
SizeFormat::DecimalBytes
|
||||
fn default() -> Self {
|
||||
Self::DecimalBytes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The types of a file’s time fields. These three fields are standard
|
||||
/// across most (all?) operating systems.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum TimeType {
|
||||
|
||||
/// The file’s modified time (`st_mtime`).
|
||||
Modified,
|
||||
|
||||
|
@ -198,10 +255,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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -210,9 +267,10 @@ 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)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct TimeTypes {
|
||||
pub modified: bool,
|
||||
pub changed: bool,
|
||||
|
@ -224,16 +282,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 {
|
||||
|
@ -241,22 +302,26 @@ 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.
|
||||
#[cfg(unix)]
|
||||
users: Mutex<UsersCache>,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn lock_users(&self) -> MutexGuard<UsersCache> {
|
||||
#[cfg(unix)]
|
||||
pub fn lock_users(&self) -> MutexGuard<'_, UsersCache> {
|
||||
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
|
||||
|
@ -264,26 +329,74 @@ impl Environment {
|
|||
};
|
||||
|
||||
let numeric = locale::Numeric::load_user_locale()
|
||||
.unwrap_or_else(|_| locale::Numeric::english());
|
||||
.unwrap_or_else(|_| locale::Numeric::english());
|
||||
|
||||
#[cfg(unix)]
|
||||
let users = Mutex::new(UsersCache::new());
|
||||
|
||||
Environment { tz, numeric, users }
|
||||
Self { numeric, tz, #[cfg(unix)] users }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn determine_time_zone() -> TZResult<TimeZone> {
|
||||
TimeZone::from_file("/etc/localtime")
|
||||
if let Ok(file) = env::var("TZ") {
|
||||
TimeZone::from_file({
|
||||
if file.starts_with('/') {
|
||||
file
|
||||
} else {
|
||||
format!("/usr/share/zoneinfo/{}", {
|
||||
if file.starts_with(':') {
|
||||
file.replacen(':', "", 1)
|
||||
} else {
|
||||
file
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
TimeZone::from_file("/etc/localtime")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn determine_time_zone() -> TZResult<TimeZone> {
|
||||
use datetime::zone::{FixedTimespan, FixedTimespanSet, StaticTimeZone, TimeZoneSource};
|
||||
use std::borrow::Cow;
|
||||
|
||||
Ok(TimeZone(TimeZoneSource::Static(&StaticTimeZone {
|
||||
name: "Unsupported",
|
||||
fixed_timespans: FixedTimespanSet {
|
||||
first: FixedTimespan {
|
||||
offset: 0,
|
||||
is_dst: false,
|
||||
name: Cow::Borrowed("ZONE_A"),
|
||||
},
|
||||
rest: &[(
|
||||
1206838800,
|
||||
FixedTimespan {
|
||||
offset: 3600,
|
||||
is_dst: false,
|
||||
name: Cow::Borrowed("ZONE_B"),
|
||||
},
|
||||
)],
|
||||
},
|
||||
})))
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref ENVIRONMENT: Environment = Environment::load_all();
|
||||
}
|
||||
|
||||
|
||||
pub struct Table<'a> {
|
||||
columns: Vec<Column>,
|
||||
colours: &'a Colours,
|
||||
theme: &'a Theme,
|
||||
env: &'a Environment,
|
||||
widths: TableWidths,
|
||||
time_format: &'a TimeFormat,
|
||||
time_format: TimeFormat,
|
||||
size_format: SizeFormat,
|
||||
user_format: UserFormat,
|
||||
git: Option<&'a GitCache>,
|
||||
}
|
||||
|
||||
|
@ -293,15 +406,20 @@ pub struct Row {
|
|||
}
|
||||
|
||||
impl<'a, 'f> Table<'a> {
|
||||
pub fn new(options: &'a Options, git: Option<&'a GitCache>, colours: &'a Colours) -> Table<'a> {
|
||||
let columns = options.extra_columns.collect(git.is_some());
|
||||
pub fn new(options: &'a Options, git: Option<&'a GitCache>, theme: &'a Theme) -> 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,
|
||||
time_format: &options.time_format,
|
||||
size_format: options.size_format,
|
||||
theme,
|
||||
widths,
|
||||
columns,
|
||||
git,
|
||||
env,
|
||||
time_format: options.time_format,
|
||||
size_format: options.size_format,
|
||||
user_format: options.user_format,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -311,15 +429,15 @@ impl<'a, 'f> Table<'a> {
|
|||
|
||||
pub fn header_row(&self) -> Row {
|
||||
let cells = self.columns.iter()
|
||||
.map(|c| TextCell::paint_str(self.colours.header, c.header()))
|
||||
.map(|c| TextCell::paint_str(self.theme.ui.header, c.header()))
|
||||
.collect();
|
||||
|
||||
Row { cells }
|
||||
}
|
||||
|
||||
pub fn row_for_file(&self, file: &File, xattrs: bool) -> Row {
|
||||
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 }
|
||||
|
@ -329,36 +447,78 @@ impl<'a, 'f> Table<'a> {
|
|||
self.widths.add_widths(row)
|
||||
}
|
||||
|
||||
fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus {
|
||||
fn permissions_plus(&self, file: &File<'_>, xattrs: bool) -> f::PermissionsPlus {
|
||||
f::PermissionsPlus {
|
||||
file_type: file.type_char(),
|
||||
#[cfg(unix)]
|
||||
permissions: file.permissions(),
|
||||
#[cfg(windows)]
|
||||
attributes: file.attributes(),
|
||||
xattrs,
|
||||
}
|
||||
}
|
||||
|
||||
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
|
||||
use output::table::TimeType::*;
|
||||
|
||||
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::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),
|
||||
#[cfg(unix)]
|
||||
fn octal_permissions(&self, file: &File<'_>) -> f::OctalPermissions {
|
||||
f::OctalPermissions {
|
||||
permissions: file.permissions(),
|
||||
}
|
||||
}
|
||||
|
||||
fn git_status(&self, file: &File) -> f::Git {
|
||||
fn display(&self, file: &File<'_>, column: Column, xattrs: bool) -> TextCell {
|
||||
match column {
|
||||
Column::Permissions => {
|
||||
self.permissions_plus(file, xattrs).render(self.theme)
|
||||
}
|
||||
Column::FileSize => {
|
||||
file.size().render(self.theme, self.size_format, &self.env.numeric)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Column::HardLinks => {
|
||||
file.links().render(self.theme, &self.env.numeric)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Column::Inode => {
|
||||
file.inode().render(self.theme.ui.inode)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Column::Blocks => {
|
||||
file.blocks().render(self.theme)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Column::User => {
|
||||
file.user().render(self.theme, &*self.env.lock_users(), self.user_format)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Column::Group => {
|
||||
file.group().render(self.theme, &*self.env.lock_users(), self.user_format)
|
||||
}
|
||||
Column::GitStatus => {
|
||||
self.git_status(file).render(self.theme)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Column::Octal => {
|
||||
self.octal_permissions(file).render(self.theme.ui.octal)
|
||||
}
|
||||
|
||||
Column::Timestamp(TimeType::Modified) => {
|
||||
file.modified_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
|
||||
}
|
||||
Column::Timestamp(TimeType::Changed) => {
|
||||
file.changed_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
|
||||
}
|
||||
Column::Timestamp(TimeType::Created) => {
|
||||
file.created_time().render(self.theme.ui.date, &self.env.tz, self.time_format)
|
||||
}
|
||||
Column::Timestamp(TimeType::Accessed) => {
|
||||
file.accessed_time().render(self.theme.ui.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()
|
||||
|
@ -367,12 +527,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);
|
||||
|
@ -383,7 +553,6 @@ impl<'a, 'f> Table<'a> {
|
|||
}
|
||||
|
||||
|
||||
|
||||
pub struct TableWidths(Vec<usize>);
|
||||
|
||||
impl Deref for TableWidths {
|
||||
|
@ -395,8 +564,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) {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
//! Timestamp formatting.
|
||||
|
||||
use std::time::Duration;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece};
|
||||
use datetime::fmt::DateFormat;
|
||||
use locale;
|
||||
use std::cmp;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
|
||||
/// Every timestamp in exa needs to be rendered by a **time format**.
|
||||
|
@ -24,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, Eq, Debug, Copy, Clone)]
|
||||
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,195 +52,192 @@ pub enum TimeFormat {
|
|||
// timestamps are separate types.
|
||||
|
||||
impl TimeFormat {
|
||||
pub fn format_local(&self, time: Duration) -> String {
|
||||
match *self {
|
||||
TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time),
|
||||
TimeFormat::ISOFormat(ref iso) => iso.format_local(time),
|
||||
TimeFormat::LongISO => long_local(time),
|
||||
TimeFormat::FullISO => full_local(time),
|
||||
pub fn format_local(self, time: SystemTime) -> String {
|
||||
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: Duration, 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),
|
||||
pub fn format_zoned(self, time: SystemTime, zone: &TimeZone) -> String {
|
||||
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 {
|
||||
|
||||
/// 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>,
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn default_local(time: SystemTime) -> String {
|
||||
let date = LocalDateTime::at(systemtime_epoch(time));
|
||||
let date_format = get_dateformat(&date);
|
||||
date_format.format(&date, &*LOCALE)
|
||||
}
|
||||
|
||||
impl DefaultFormat {
|
||||
pub fn load() -> DefaultFormat {
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn default_zoned(time: SystemTime, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
|
||||
let date_format = get_dateformat(&date);
|
||||
date_format.format(&date, &*LOCALE)
|
||||
}
|
||||
|
||||
let locale = locale::Time::load_user_locale()
|
||||
.unwrap_or_else(|_| locale::Time::english());
|
||||
fn get_dateformat(date: &LocalDateTime) -> &'static DateFormat<'static> {
|
||||
match (is_recent(date), *MAXIMUM_MONTH_WIDTH) {
|
||||
(true, 4) => &FOUR_WIDE_DATE_TIME,
|
||||
(true, 5) => &FIVE_WIDE_DATE_TIME,
|
||||
(true, _) => &OTHER_WIDE_DATE_TIME,
|
||||
(false, 4) => &FOUR_WIDE_DATE_YEAR,
|
||||
(false, 5) => &FIVE_WIDE_DATE_YEAR,
|
||||
(false, _) => &OTHER_WIDE_DATE_YEAR,
|
||||
}
|
||||
}
|
||||
|
||||
let current_year = LocalDateTime::now().year();
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn long_local(time: SystemTime) -> String {
|
||||
let date = LocalDateTime::at(systemtime_epoch(time));
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute())
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn long_zoned(time: SystemTime, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time)));
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute())
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn full_local(time: SystemTime) -> String {
|
||||
let date = LocalDateTime::at(systemtime_epoch(time));
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute(), date.second(), systemtime_nanos(time))
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn full_zoned(time: SystemTime, zone: &TimeZone) -> String {
|
||||
use datetime::Offset;
|
||||
|
||||
let local = LocalDateTime::at(systemtime_epoch(time));
|
||||
let date = zone.to_zoned(local);
|
||||
let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute(), date.second(), systemtime_nanos(time),
|
||||
offset.hours(), offset.minutes().abs())
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn iso_local(time: SystemTime) -> String {
|
||||
let date = LocalDateTime::at(systemtime_epoch(time));
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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 = cmp::max(maximum_month_width, current_month_width);
|
||||
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
|
||||
};
|
||||
|
||||
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(),
|
||||
};
|
||||
static ref FOUR_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
|
||||
"{2>:D} {4<:M} {02>:h}:{02>:m}"
|
||||
).unwrap();
|
||||
|
||||
let date_and_year = match maximum_month_width {
|
||||
4 => DateFormat::parse("{2>:D} {4<:M} {5>:Y}").unwrap(),
|
||||
5 => DateFormat::parse("{2>:D} {5<:M} {5>:Y}").unwrap(),
|
||||
_ => DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
|
||||
};
|
||||
static ref FIVE_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
|
||||
"{2>:D} {5<:M} {02>:h}:{02>:m}"
|
||||
).unwrap();
|
||||
|
||||
DefaultFormat { current_year, locale, date_and_time, date_and_year }
|
||||
}
|
||||
}
|
||||
|
||||
impl DefaultFormat {
|
||||
fn is_recent(&self, date: LocalDateTime) -> bool {
|
||||
date.year() == self.current_year
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn format_local(&self, time: Duration) -> String {
|
||||
let date = LocalDateTime::at(time.as_secs() as i64);
|
||||
|
||||
if self.is_recent(date) {
|
||||
self.date_and_time.format(&date, &self.locale)
|
||||
}
|
||||
else {
|
||||
self.date_and_year.format(&date, &self.locale)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn format_zoned(&self, time: Duration, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64));
|
||||
|
||||
if self.is_recent(date) {
|
||||
self.date_and_time.format(&date, &self.locale)
|
||||
}
|
||||
else {
|
||||
self.date_and_year.format(&date, &self.locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn long_local(time: Duration) -> String {
|
||||
let date = LocalDateTime::at(time.as_secs() as i64);
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute())
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn long_zoned(time: Duration, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64));
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute())
|
||||
}
|
||||
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn full_local(time: Duration) -> String {
|
||||
let date = LocalDateTime::at(time.as_secs() as i64);
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute(), date.second(), time.subsec_nanos())
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn full_zoned(time: Duration, zone: &TimeZone) -> String {
|
||||
use datetime::Offset;
|
||||
|
||||
let local = LocalDateTime::at(time.as_secs() as i64);
|
||||
let date = zone.to_zoned(local);
|
||||
let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}",
|
||||
date.year(), date.month() as usize, date.day(),
|
||||
date.hour(), date.minute(), date.second(), time.subsec_nanos(),
|
||||
offset.hours(), offset.minutes().abs())
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[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 }
|
||||
}
|
||||
}
|
||||
|
||||
impl ISOFormat {
|
||||
fn is_recent(&self, date: LocalDateTime) -> bool {
|
||||
date.year() == self.current_year
|
||||
}
|
||||
|
||||
#[allow(trivial_numeric_casts)]
|
||||
fn format_local(&self, time: Duration) -> String {
|
||||
let date = LocalDateTime::at(time.as_secs() as i64);
|
||||
|
||||
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: Duration, zone: &TimeZone) -> String {
|
||||
let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64));
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
static ref OTHER_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse(
|
||||
"{2>:D} {:M} {02>: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, Eq, 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::*;
|
||||
|
@ -248,19 +253,19 @@ mod iter_test {
|
|||
#[test]
|
||||
fn test_iteration() {
|
||||
let foos = &[ "first", "middle", "last" ];
|
||||
let mut iter = TreeDepth::root().iterate_over(foos.into_iter());
|
||||
let mut iter = TreeDepth::root().iterate_over(foos.iter());
|
||||
|
||||
let next = iter.next().unwrap();
|
||||
assert_eq!(&"first", next.1);
|
||||
assert_eq!(false, next.0.last);
|
||||
assert!(!next.0.last);
|
||||
|
||||
let next = iter.next().unwrap();
|
||||
assert_eq!(&"middle", next.1);
|
||||
assert_eq!(false, next.0.last);
|
||||
assert!(!next.0.last);
|
||||
|
||||
let next = iter.next().unwrap();
|
||||
assert_eq!(&"last", next.1);
|
||||
assert_eq!(true, next.0.last);
|
||||
assert!(next.0.last);
|
||||
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
|
@ -268,7 +273,7 @@ mod iter_test {
|
|||
#[test]
|
||||
fn test_empty() {
|
||||
let nothing: &[usize] = &[];
|
||||
let mut iter = TreeDepth::root().iterate_over(nothing.into_iter());
|
||||
let mut iter = TreeDepth::root().iterate_over(nothing.iter());
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,406 +0,0 @@
|
|||
use ansi_term::Style;
|
||||
use ansi_term::Colour::{Red, Green, Yellow, Blue, Cyan, Purple, Fixed};
|
||||
|
||||
use output::render;
|
||||
use output::file_name::Colours as FileNameColours;
|
||||
|
||||
use style::lsc::Pair;
|
||||
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Colours {
|
||||
pub colourful: bool,
|
||||
pub scale: bool,
|
||||
|
||||
pub filekinds: FileKinds,
|
||||
pub perms: Permissions,
|
||||
pub size: Size,
|
||||
pub users: Users,
|
||||
pub links: Links,
|
||||
pub git: Git,
|
||||
|
||||
pub punctuation: Style,
|
||||
pub date: Style,
|
||||
pub inode: Style,
|
||||
pub blocks: Style,
|
||||
pub header: Style,
|
||||
|
||||
pub symlink_path: Style,
|
||||
pub control_char: Style,
|
||||
pub broken_symlink: Style,
|
||||
pub broken_path_overlay: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct FileKinds {
|
||||
pub normal: Style,
|
||||
pub directory: Style,
|
||||
pub symlink: Style,
|
||||
pub pipe: Style,
|
||||
pub block_device: Style,
|
||||
pub char_device: Style,
|
||||
pub socket: Style,
|
||||
pub special: Style,
|
||||
pub executable: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Permissions {
|
||||
pub user_read: Style,
|
||||
pub user_write: Style,
|
||||
pub user_execute_file: Style,
|
||||
pub user_execute_other: Style,
|
||||
|
||||
pub group_read: Style,
|
||||
pub group_write: Style,
|
||||
pub group_execute: Style,
|
||||
|
||||
pub other_read: Style,
|
||||
pub other_write: Style,
|
||||
pub other_execute: Style,
|
||||
|
||||
pub special_user_file: Style,
|
||||
pub special_other: Style,
|
||||
|
||||
pub attribute: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Size {
|
||||
pub numbers: Style,
|
||||
pub unit: Style,
|
||||
|
||||
pub major: Style,
|
||||
pub minor: Style,
|
||||
|
||||
pub scale_byte: Style,
|
||||
pub scale_kilo: Style,
|
||||
pub scale_mega: Style,
|
||||
pub scale_giga: Style,
|
||||
pub scale_huge: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Users {
|
||||
pub user_you: Style,
|
||||
pub user_someone_else: Style,
|
||||
pub group_yours: Style,
|
||||
pub group_not_yours: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Links {
|
||||
pub normal: Style,
|
||||
pub multi_link_file: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Git {
|
||||
pub new: Style,
|
||||
pub modified: Style,
|
||||
pub deleted: Style,
|
||||
pub renamed: Style,
|
||||
pub typechange: Style,
|
||||
pub ignored: Style,
|
||||
}
|
||||
|
||||
impl Colours {
|
||||
pub fn plain() -> Colours {
|
||||
Colours::default()
|
||||
}
|
||||
|
||||
pub fn colourful(scale: bool) -> Colours {
|
||||
Colours {
|
||||
colourful: true,
|
||||
scale,
|
||||
|
||||
filekinds: FileKinds {
|
||||
normal: Style::default(),
|
||||
directory: Blue.bold(),
|
||||
symlink: Cyan.normal(),
|
||||
pipe: Yellow.normal(),
|
||||
block_device: Yellow.bold(),
|
||||
char_device: Yellow.bold(),
|
||||
socket: Red.bold(),
|
||||
special: Yellow.normal(),
|
||||
executable: Green.bold(),
|
||||
},
|
||||
|
||||
perms: Permissions {
|
||||
user_read: Yellow.bold(),
|
||||
user_write: Red.bold(),
|
||||
user_execute_file: Green.bold().underline(),
|
||||
user_execute_other: Green.bold(),
|
||||
|
||||
group_read: Yellow.normal(),
|
||||
group_write: Red.normal(),
|
||||
group_execute: Green.normal(),
|
||||
|
||||
other_read: Yellow.normal(),
|
||||
other_write: Red.normal(),
|
||||
other_execute: Green.normal(),
|
||||
|
||||
special_user_file: Purple.normal(),
|
||||
special_other: Purple.normal(),
|
||||
|
||||
attribute: Style::default(),
|
||||
},
|
||||
|
||||
size: Size {
|
||||
numbers: Green.bold(),
|
||||
unit: Green.normal(),
|
||||
|
||||
major: Green.bold(),
|
||||
minor: Green.normal(),
|
||||
|
||||
scale_byte: Fixed(118).normal(),
|
||||
scale_kilo: Fixed(190).normal(),
|
||||
scale_mega: Fixed(226).normal(),
|
||||
scale_giga: Fixed(220).normal(),
|
||||
scale_huge: Fixed(214).normal(),
|
||||
},
|
||||
|
||||
users: Users {
|
||||
user_you: Yellow.bold(),
|
||||
user_someone_else: Style::default(),
|
||||
group_yours: Yellow.bold(),
|
||||
group_not_yours: Style::default(),
|
||||
},
|
||||
|
||||
links: Links {
|
||||
normal: Red.bold(),
|
||||
multi_link_file: Red.on(Yellow),
|
||||
},
|
||||
|
||||
git: Git {
|
||||
new: Green.normal(),
|
||||
modified: Blue.normal(),
|
||||
deleted: Red.normal(),
|
||||
renamed: Yellow.normal(),
|
||||
typechange: Purple.normal(),
|
||||
ignored: Style::default().dimmed(),
|
||||
},
|
||||
|
||||
punctuation: Fixed(244).normal(),
|
||||
date: Blue.normal(),
|
||||
inode: Purple.normal(),
|
||||
blocks: Cyan.normal(),
|
||||
header: Style::default().underline(),
|
||||
|
||||
symlink_path: Cyan.normal(),
|
||||
control_char: Red.normal(),
|
||||
broken_symlink: Red.normal(),
|
||||
broken_path_overlay: Style::default().underline(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Some of the styles are **overlays**: although they have the same attribute
|
||||
/// set as regular styles (foreground and background colours, bold, underline,
|
||||
/// etc), they’re intended to be used to *amend* existing styles.
|
||||
///
|
||||
/// For example, the target path of a broken symlink is displayed in a red,
|
||||
/// underlined style by default. Paths can contain control characters, so
|
||||
/// these control characters need to be underlined too, otherwise it looks
|
||||
/// weird. So instead of having four separate configurable styles for “link
|
||||
/// path”, “broken link path”, “control character” and “broken control
|
||||
/// character”, there are styles for “link path”, “control character”, and
|
||||
/// “broken link overlay”, the latter of which is just set to override the
|
||||
/// underline attribute on the other two.
|
||||
fn apply_overlay(mut base: Style, overlay: Style) -> Style {
|
||||
if let Some(fg) = overlay.foreground { base.foreground = Some(fg); }
|
||||
if let Some(bg) = overlay.background { base.background = Some(bg); }
|
||||
|
||||
if overlay.is_bold { base.is_bold = true; }
|
||||
if overlay.is_dimmed { base.is_dimmed = true; }
|
||||
if overlay.is_italic { base.is_italic = true; }
|
||||
if overlay.is_underline { base.is_underline = true; }
|
||||
if overlay.is_blink { base.is_blink = true; }
|
||||
if overlay.is_reverse { base.is_reverse = true; }
|
||||
if overlay.is_hidden { base.is_hidden = true; }
|
||||
if overlay.is_strikethrough { base.is_strikethrough = true; }
|
||||
|
||||
base
|
||||
}
|
||||
// TODO: move this function to the ansi_term crate
|
||||
|
||||
|
||||
impl Colours {
|
||||
|
||||
/// Sets a value on this set of colours using one of the keys understood
|
||||
/// by the `LS_COLORS` environment variable. Invalid keys set nothing, but
|
||||
/// return false.
|
||||
pub fn set_ls(&mut self, pair: &Pair) -> bool {
|
||||
match pair.key {
|
||||
"di" => self.filekinds.directory = pair.to_style(), // DIR
|
||||
"ex" => self.filekinds.executable = pair.to_style(), // EXEC
|
||||
"fi" => self.filekinds.normal = pair.to_style(), // FILE
|
||||
"pi" => self.filekinds.pipe = pair.to_style(), // FIFO
|
||||
"so" => self.filekinds.socket = pair.to_style(), // SOCK
|
||||
"bd" => self.filekinds.block_device = pair.to_style(), // BLK
|
||||
"cd" => self.filekinds.char_device = pair.to_style(), // CHR
|
||||
"ln" => self.filekinds.symlink = pair.to_style(), // LINK
|
||||
"or" => self.broken_symlink = pair.to_style(), // ORPHAN
|
||||
_ => return false,
|
||||
// Codes we don’t do anything with:
|
||||
// MULTIHARDLINK, DOOR, SETUID, SETGID, CAPABILITY,
|
||||
// STICKY_OTHER_WRITABLE, OTHER_WRITABLE, STICKY, MISSING
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Sets a value on this set of colours using one of the keys understood
|
||||
/// by the `EXA_COLORS` environment variable. Invalid keys set nothing,
|
||||
/// but return false. This doesn’t take the `LS_COLORS` keys into account,
|
||||
/// so `set_ls` should have been run first.
|
||||
pub fn set_exa(&mut self, pair: &Pair) -> bool {
|
||||
match pair.key {
|
||||
"ur" => self.perms.user_read = pair.to_style(),
|
||||
"uw" => self.perms.user_write = pair.to_style(),
|
||||
"ux" => self.perms.user_execute_file = pair.to_style(),
|
||||
"ue" => self.perms.user_execute_other = pair.to_style(),
|
||||
"gr" => self.perms.group_read = pair.to_style(),
|
||||
"gw" => self.perms.group_write = pair.to_style(),
|
||||
"gx" => self.perms.group_execute = pair.to_style(),
|
||||
"tr" => self.perms.other_read = pair.to_style(),
|
||||
"tw" => self.perms.other_write = pair.to_style(),
|
||||
"tx" => self.perms.other_execute = pair.to_style(),
|
||||
"su" => self.perms.special_user_file = pair.to_style(),
|
||||
"sf" => self.perms.special_other = pair.to_style(),
|
||||
"xa" => self.perms.attribute = pair.to_style(),
|
||||
|
||||
"sn" => self.size.numbers = pair.to_style(),
|
||||
"sb" => self.size.unit = pair.to_style(),
|
||||
"df" => self.size.major = pair.to_style(),
|
||||
"ds" => self.size.minor = pair.to_style(),
|
||||
|
||||
"uu" => self.users.user_you = pair.to_style(),
|
||||
"un" => self.users.user_someone_else = pair.to_style(),
|
||||
"gu" => self.users.group_yours = pair.to_style(),
|
||||
"gn" => self.users.group_not_yours = pair.to_style(),
|
||||
|
||||
"lc" => self.links.normal = pair.to_style(),
|
||||
"lm" => self.links.multi_link_file = pair.to_style(),
|
||||
|
||||
"ga" => self.git.new = pair.to_style(),
|
||||
"gm" => self.git.modified = pair.to_style(),
|
||||
"gd" => self.git.deleted = pair.to_style(),
|
||||
"gv" => self.git.renamed = pair.to_style(),
|
||||
"gt" => self.git.typechange = pair.to_style(),
|
||||
|
||||
"xx" => self.punctuation = pair.to_style(),
|
||||
"da" => self.date = pair.to_style(),
|
||||
"in" => self.inode = pair.to_style(),
|
||||
"bl" => self.blocks = pair.to_style(),
|
||||
"hd" => self.header = pair.to_style(),
|
||||
"lp" => self.symlink_path = pair.to_style(),
|
||||
"cc" => self.control_char = pair.to_style(),
|
||||
"bO" => self.broken_path_overlay = pair.to_style(),
|
||||
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl render::BlocksColours for Colours {
|
||||
fn block_count(&self) -> Style { self.blocks }
|
||||
fn no_blocks(&self) -> Style { self.punctuation }
|
||||
}
|
||||
|
||||
impl render::FiletypeColours for Colours {
|
||||
fn normal(&self) -> Style { self.filekinds.normal }
|
||||
fn directory(&self) -> Style { self.filekinds.directory }
|
||||
fn pipe(&self) -> Style { self.filekinds.pipe }
|
||||
fn symlink(&self) -> Style { self.filekinds.symlink }
|
||||
fn block_device(&self) -> Style { self.filekinds.block_device }
|
||||
fn char_device(&self) -> Style { self.filekinds.char_device }
|
||||
fn socket(&self) -> Style { self.filekinds.socket }
|
||||
fn special(&self) -> Style { self.filekinds.special }
|
||||
}
|
||||
|
||||
impl render::GitColours for Colours {
|
||||
fn not_modified(&self) -> Style { self.punctuation }
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
fn new(&self) -> Style { self.git.new }
|
||||
fn modified(&self) -> Style { self.git.modified }
|
||||
fn deleted(&self) -> Style { self.git.deleted }
|
||||
fn renamed(&self) -> Style { self.git.renamed }
|
||||
fn type_change(&self) -> Style { self.git.typechange }
|
||||
fn ignored(&self) -> Style { self.git.ignored }
|
||||
}
|
||||
|
||||
impl render::GroupColours for Colours {
|
||||
fn yours(&self) -> Style { self.users.group_yours }
|
||||
fn not_yours(&self) -> Style { self.users.group_not_yours }
|
||||
}
|
||||
|
||||
impl render::LinksColours for Colours {
|
||||
fn normal(&self) -> Style { self.links.normal }
|
||||
fn multi_link_file(&self) -> Style { self.links.multi_link_file }
|
||||
}
|
||||
|
||||
impl render::PermissionsColours for Colours {
|
||||
fn dash(&self) -> Style { self.punctuation }
|
||||
fn user_read(&self) -> Style { self.perms.user_read }
|
||||
fn user_write(&self) -> Style { self.perms.user_write }
|
||||
fn user_execute_file(&self) -> Style { self.perms.user_execute_file }
|
||||
fn user_execute_other(&self) -> Style { self.perms.user_execute_other }
|
||||
fn group_read(&self) -> Style { self.perms.group_read }
|
||||
fn group_write(&self) -> Style { self.perms.group_write }
|
||||
fn group_execute(&self) -> Style { self.perms.group_execute }
|
||||
fn other_read(&self) -> Style { self.perms.other_read }
|
||||
fn other_write(&self) -> Style { self.perms.other_write }
|
||||
fn other_execute(&self) -> Style { self.perms.other_execute }
|
||||
fn special_user_file(&self) -> Style { self.perms.special_user_file }
|
||||
fn special_other(&self) -> Style { self.perms.special_other }
|
||||
fn attribute(&self) -> Style { self.perms.attribute }
|
||||
}
|
||||
|
||||
impl render::SizeColours for Colours {
|
||||
fn size(&self, size: u64) -> Style {
|
||||
if self.scale {
|
||||
if size < 1024 {
|
||||
self.size.scale_byte
|
||||
}
|
||||
else if size < 1024 * 1024 {
|
||||
self.size.scale_kilo
|
||||
}
|
||||
else if size < 1024 * 1024 * 1024 {
|
||||
self.size.scale_mega
|
||||
}
|
||||
else if size < 1024 * 1024 * 1024 * 1024 {
|
||||
self.size.scale_giga
|
||||
}
|
||||
else {
|
||||
self.size.scale_huge
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.size.numbers
|
||||
}
|
||||
}
|
||||
|
||||
fn unit(&self) -> Style { self.size.unit }
|
||||
fn no_size(&self) -> Style { self.punctuation }
|
||||
fn major(&self) -> Style { self.size.major }
|
||||
fn comma(&self) -> Style { self.punctuation }
|
||||
fn minor(&self) -> Style { self.size.minor }
|
||||
}
|
||||
|
||||
impl render::UserColours for Colours {
|
||||
fn you(&self) -> Style { self.users.user_you }
|
||||
fn someone_else(&self) -> Style { self.users.user_someone_else }
|
||||
}
|
||||
|
||||
impl FileNameColours for Colours {
|
||||
fn normal_arrow(&self) -> Style { self.punctuation }
|
||||
fn broken_symlink(&self) -> Style { self.broken_symlink }
|
||||
fn broken_filename(&self) -> Style { apply_overlay(self.broken_symlink, self.broken_path_overlay) }
|
||||
fn broken_control_char(&self) -> Style { apply_overlay(self.control_char, self.broken_path_overlay) }
|
||||
fn control_char(&self) -> Style { self.control_char }
|
||||
fn symlink_path(&self) -> Style { self.symlink_path }
|
||||
fn executable_file(&self) -> Style { self.filekinds.executable }
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
mod colours;
|
||||
pub use self::colours::Colours;
|
||||
|
||||
mod lsc;
|
||||
pub use self::lsc::LSColors;
|
|
@ -0,0 +1,130 @@
|
|||
use ansi_term::Style;
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
use crate::theme::ColourScale;
|
||||
use crate::theme::ui_styles::*;
|
||||
|
||||
|
||||
impl UiStyles {
|
||||
pub fn default_theme(scale: ColourScale) -> Self {
|
||||
Self {
|
||||
colourful: true,
|
||||
|
||||
filekinds: FileKinds {
|
||||
normal: Style::default(),
|
||||
directory: Blue.bold(),
|
||||
symlink: Cyan.normal(),
|
||||
pipe: Yellow.normal(),
|
||||
block_device: Yellow.bold(),
|
||||
char_device: Yellow.bold(),
|
||||
socket: Red.bold(),
|
||||
special: Yellow.normal(),
|
||||
executable: Green.bold(),
|
||||
},
|
||||
|
||||
perms: Permissions {
|
||||
user_read: Yellow.bold(),
|
||||
user_write: Red.bold(),
|
||||
user_execute_file: Green.bold().underline(),
|
||||
user_execute_other: Green.bold(),
|
||||
|
||||
group_read: Yellow.normal(),
|
||||
group_write: Red.normal(),
|
||||
group_execute: Green.normal(),
|
||||
|
||||
other_read: Yellow.normal(),
|
||||
other_write: Red.normal(),
|
||||
other_execute: Green.normal(),
|
||||
|
||||
special_user_file: Purple.normal(),
|
||||
special_other: Purple.normal(),
|
||||
|
||||
attribute: Style::default(),
|
||||
},
|
||||
|
||||
size: Size::colourful(scale),
|
||||
|
||||
users: Users {
|
||||
user_you: Yellow.bold(),
|
||||
user_someone_else: Style::default(),
|
||||
group_yours: Yellow.bold(),
|
||||
group_not_yours: Style::default(),
|
||||
},
|
||||
|
||||
links: Links {
|
||||
normal: Red.bold(),
|
||||
multi_link_file: Red.on(Yellow),
|
||||
},
|
||||
|
||||
git: Git {
|
||||
new: Green.normal(),
|
||||
modified: Blue.normal(),
|
||||
deleted: Red.normal(),
|
||||
renamed: Yellow.normal(),
|
||||
typechange: Purple.normal(),
|
||||
ignored: Style::default().dimmed(),
|
||||
conflicted: Red.normal(),
|
||||
},
|
||||
|
||||
punctuation: Fixed(244).normal(),
|
||||
date: Blue.normal(),
|
||||
inode: Purple.normal(),
|
||||
blocks: Cyan.normal(),
|
||||
octal: Purple.normal(),
|
||||
header: Style::default().underline(),
|
||||
|
||||
symlink_path: Cyan.normal(),
|
||||
control_char: Red.normal(),
|
||||
broken_symlink: Red.normal(),
|
||||
broken_path_overlay: Style::default().underline(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Size {
|
||||
pub fn colourful(scale: ColourScale) -> Self {
|
||||
match scale {
|
||||
ColourScale::Gradient => Self::colourful_gradient(),
|
||||
ColourScale::Fixed => Self::colourful_fixed(),
|
||||
}
|
||||
}
|
||||
|
||||
fn colourful_fixed() -> Self {
|
||||
Self {
|
||||
major: Green.bold(),
|
||||
minor: Green.normal(),
|
||||
|
||||
number_byte: Green.bold(),
|
||||
number_kilo: Green.bold(),
|
||||
number_mega: Green.bold(),
|
||||
number_giga: Green.bold(),
|
||||
number_huge: Green.bold(),
|
||||
|
||||
unit_byte: Green.normal(),
|
||||
unit_kilo: Green.normal(),
|
||||
unit_mega: Green.normal(),
|
||||
unit_giga: Green.normal(),
|
||||
unit_huge: Green.normal(),
|
||||
}
|
||||
}
|
||||
|
||||
fn colourful_gradient() -> Self {
|
||||
Self {
|
||||
major: Green.bold(),
|
||||
minor: Green.normal(),
|
||||
|
||||
number_byte: Fixed(118).normal(),
|
||||
number_kilo: Fixed(190).normal(),
|
||||
number_mega: Fixed(226).normal(),
|
||||
number_giga: Fixed(220).normal(),
|
||||
number_huge: Fixed(214).normal(),
|
||||
|
||||
unit_byte: Green.normal(),
|
||||
unit_kilo: Green.normal(),
|
||||
unit_mega: Green.normal(),
|
||||
unit_giga: Green.normal(),
|
||||
unit_huge: Green.normal(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
@ -71,17 +70,26 @@ where I: Iterator<Item=&'a str> {
|
|||
}*/
|
||||
|
||||
if let (Some(r), Some(g), Some(b)) = (hexes.parse().ok(),
|
||||
iter.next().and_then(|s| s.parse().ok()),
|
||||
iter.next().and_then(|s| s.parse().ok())) {
|
||||
iter.next().and_then(|s| s.parse().ok()),
|
||||
iter.next().and_then(|s| s.parse().ok()))
|
||||
{
|
||||
return Some(RGB(r, g, b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {},
|
||||
}
|
||||
|
||||
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 +131,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 +191,6 @@ mod ansi_test {
|
|||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
|
@ -0,0 +1,530 @@
|
|||
use ansi_term::Style;
|
||||
|
||||
use crate::fs::File;
|
||||
use crate::output::file_name::Colours as FileNameColours;
|
||||
use crate::output::render;
|
||||
|
||||
mod ui_styles;
|
||||
pub use self::ui_styles::UiStyles;
|
||||
pub use self::ui_styles::Size as SizeColours;
|
||||
|
||||
mod lsc;
|
||||
pub use self::lsc::LSColors;
|
||||
|
||||
mod default_theme;
|
||||
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Options {
|
||||
|
||||
pub use_colours: UseColours,
|
||||
|
||||
pub colour_scale: ColourScale,
|
||||
|
||||
pub definitions: Definitions,
|
||||
}
|
||||
|
||||
/// Under what circumstances we should display coloured, rather than plain,
|
||||
/// output to the terminal.
|
||||
///
|
||||
/// By default, we want to display the colours when stdout can display them.
|
||||
/// Turning them on when output is going to, say, a pipe, would make programs
|
||||
/// such as `grep` or `more` not work properly. So the `Automatic` mode does
|
||||
/// this check and only displays colours when they can be truly appreciated.
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum UseColours {
|
||||
|
||||
/// Display them even when output isn’t going to a terminal.
|
||||
Always,
|
||||
|
||||
/// Display them when output is going to a terminal, but not otherwise.
|
||||
Automatic,
|
||||
|
||||
/// Never display them, even when output is going to a terminal.
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum ColourScale {
|
||||
Fixed,
|
||||
Gradient,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Default)]
|
||||
pub struct Definitions {
|
||||
pub ls: Option<String>,
|
||||
pub exa: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
pub struct Theme {
|
||||
pub ui: UiStyles,
|
||||
pub exts: Box<dyn FileColours>,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
|
||||
#[allow(trivial_casts)] // the `as Box<_>` stuff below warns about this for some reason
|
||||
pub fn to_theme(&self, isatty: bool) -> Theme {
|
||||
use crate::info::filetype::FileExtensions;
|
||||
|
||||
if self.use_colours == UseColours::Never || (self.use_colours == UseColours::Automatic && ! isatty) {
|
||||
let ui = UiStyles::plain();
|
||||
let exts = Box::new(NoFileColours);
|
||||
return Theme { ui, exts };
|
||||
}
|
||||
|
||||
// Parse the environment variables into colours and extension mappings
|
||||
let mut ui = UiStyles::default_theme(self.colour_scale);
|
||||
let (exts, use_default_filetypes) = self.definitions.parse_color_vars(&mut ui);
|
||||
|
||||
// Use between 0 and 2 file name highlighters
|
||||
let exts = match (exts.is_non_empty(), use_default_filetypes) {
|
||||
(false, false) => Box::new(NoFileColours) as Box<_>,
|
||||
(false, true) => Box::new(FileExtensions) as Box<_>,
|
||||
( true, false) => Box::new(exts) as Box<_>,
|
||||
( true, true) => Box::new((exts, FileExtensions)) as Box<_>,
|
||||
};
|
||||
|
||||
Theme { ui, exts }
|
||||
}
|
||||
}
|
||||
|
||||
impl Definitions {
|
||||
|
||||
/// Parse the environment variables into `LS_COLORS` pairs, putting file glob
|
||||
/// colours into the `ExtensionMappings` that gets returned, and using the
|
||||
/// two-character UI codes to modify the mutable `Colours`.
|
||||
///
|
||||
/// Also returns if the `EXA_COLORS` variable should reset the existing file
|
||||
/// type mappings or not. The `reset` code needs to be the first one.
|
||||
fn parse_color_vars(&self, colours: &mut UiStyles) -> (ExtensionMappings, bool) {
|
||||
use log::*;
|
||||
|
||||
let mut exts = ExtensionMappings::default();
|
||||
|
||||
if let Some(lsc) = &self.ls {
|
||||
LSColors(lsc).each_pair(|pair| {
|
||||
if ! colours.set_ls(&pair) {
|
||||
match glob::Pattern::new(pair.key) {
|
||||
Ok(pat) => {
|
||||
exts.add(pat, pair.to_style());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut use_default_filetypes = true;
|
||||
|
||||
if let Some(exa) = &self.exa {
|
||||
// Is this hacky? Yes.
|
||||
if exa == "reset" || exa.starts_with("reset:") {
|
||||
use_default_filetypes = false;
|
||||
}
|
||||
|
||||
LSColors(exa).each_pair(|pair| {
|
||||
if ! colours.set_ls(&pair) && ! colours.set_exa(&pair) {
|
||||
match glob::Pattern::new(pair.key) {
|
||||
Ok(pat) => {
|
||||
exts.add(pat, pair.to_style());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Couldn't parse glob pattern {:?}: {}", pair.key, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
(exts, use_default_filetypes)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait FileColours: std::marker::Sync {
|
||||
fn colour_file(&self, file: &File<'_>) -> Option<Style>;
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
struct NoFileColours;
|
||||
impl FileColours for NoFileColours {
|
||||
fn colour_file(&self, _file: &File<'_>) -> Option<Style> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// When getting the colour of a file from a *pair* of colourisers, try the
|
||||
// first one then try the second one. This lets the user provide their own
|
||||
// file type associations, while falling back to the default set if not set
|
||||
// explicitly.
|
||||
impl<A, B> FileColours for (A, B)
|
||||
where A: FileColours,
|
||||
B: FileColours,
|
||||
{
|
||||
fn colour_file(&self, file: &File<'_>) -> Option<Style> {
|
||||
self.0.colour_file(file)
|
||||
.or_else(|| self.1.colour_file(file))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Default)]
|
||||
struct ExtensionMappings {
|
||||
mappings: Vec<(glob::Pattern, Style)>,
|
||||
}
|
||||
|
||||
// Loop through backwards so that colours specified later in the list override
|
||||
// colours specified earlier, like we do with options and strict mode
|
||||
|
||||
impl FileColours for ExtensionMappings {
|
||||
fn colour_file(&self, file: &File<'_>) -> Option<Style> {
|
||||
self.mappings.iter().rev()
|
||||
.find(|t| t.0.matches(&file.name))
|
||||
.map (|t| t.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionMappings {
|
||||
fn is_non_empty(&self) -> bool {
|
||||
! self.mappings.is_empty()
|
||||
}
|
||||
|
||||
fn add(&mut self, pattern: glob::Pattern, style: Style) {
|
||||
self.mappings.push((pattern, style))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
impl render::BlocksColours for Theme {
|
||||
fn block_count(&self) -> Style { self.ui.blocks }
|
||||
fn no_blocks(&self) -> Style { self.ui.punctuation }
|
||||
}
|
||||
|
||||
impl render::FiletypeColours for Theme {
|
||||
fn normal(&self) -> Style { self.ui.filekinds.normal }
|
||||
fn directory(&self) -> Style { self.ui.filekinds.directory }
|
||||
fn pipe(&self) -> Style { self.ui.filekinds.pipe }
|
||||
fn symlink(&self) -> Style { self.ui.filekinds.symlink }
|
||||
fn block_device(&self) -> Style { self.ui.filekinds.block_device }
|
||||
fn char_device(&self) -> Style { self.ui.filekinds.char_device }
|
||||
fn socket(&self) -> Style { self.ui.filekinds.socket }
|
||||
fn special(&self) -> Style { self.ui.filekinds.special }
|
||||
}
|
||||
|
||||
impl render::GitColours for Theme {
|
||||
fn not_modified(&self) -> Style { self.ui.punctuation }
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
fn new(&self) -> Style { self.ui.git.new }
|
||||
fn modified(&self) -> Style { self.ui.git.modified }
|
||||
fn deleted(&self) -> Style { self.ui.git.deleted }
|
||||
fn renamed(&self) -> Style { self.ui.git.renamed }
|
||||
fn type_change(&self) -> Style { self.ui.git.typechange }
|
||||
fn ignored(&self) -> Style { self.ui.git.ignored }
|
||||
fn conflicted(&self) -> Style { self.ui.git.conflicted }
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl render::GroupColours for Theme {
|
||||
fn yours(&self) -> Style { self.ui.users.group_yours }
|
||||
fn not_yours(&self) -> Style { self.ui.users.group_not_yours }
|
||||
}
|
||||
|
||||
impl render::LinksColours for Theme {
|
||||
fn normal(&self) -> Style { self.ui.links.normal }
|
||||
fn multi_link_file(&self) -> Style { self.ui.links.multi_link_file }
|
||||
}
|
||||
|
||||
impl render::PermissionsColours for Theme {
|
||||
fn dash(&self) -> Style { self.ui.punctuation }
|
||||
fn user_read(&self) -> Style { self.ui.perms.user_read }
|
||||
fn user_write(&self) -> Style { self.ui.perms.user_write }
|
||||
fn user_execute_file(&self) -> Style { self.ui.perms.user_execute_file }
|
||||
fn user_execute_other(&self) -> Style { self.ui.perms.user_execute_other }
|
||||
fn group_read(&self) -> Style { self.ui.perms.group_read }
|
||||
fn group_write(&self) -> Style { self.ui.perms.group_write }
|
||||
fn group_execute(&self) -> Style { self.ui.perms.group_execute }
|
||||
fn other_read(&self) -> Style { self.ui.perms.other_read }
|
||||
fn other_write(&self) -> Style { self.ui.perms.other_write }
|
||||
fn other_execute(&self) -> Style { self.ui.perms.other_execute }
|
||||
fn special_user_file(&self) -> Style { self.ui.perms.special_user_file }
|
||||
fn special_other(&self) -> Style { self.ui.perms.special_other }
|
||||
fn attribute(&self) -> Style { self.ui.perms.attribute }
|
||||
}
|
||||
|
||||
impl render::SizeColours for Theme {
|
||||
fn size(&self, prefix: Option<number_prefix::Prefix>) -> Style {
|
||||
use number_prefix::Prefix::*;
|
||||
|
||||
match prefix {
|
||||
Some(Kilo | Kibi) => self.ui.size.number_kilo,
|
||||
Some(Mega | Mebi) => self.ui.size.number_mega,
|
||||
Some(Giga | Gibi) => self.ui.size.number_giga,
|
||||
Some(_) => self.ui.size.number_huge,
|
||||
None => self.ui.size.number_byte,
|
||||
}
|
||||
}
|
||||
|
||||
fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
|
||||
use number_prefix::Prefix::*;
|
||||
|
||||
match prefix {
|
||||
Some(Kilo | Kibi) => self.ui.size.unit_kilo,
|
||||
Some(Mega | Mebi) => self.ui.size.unit_mega,
|
||||
Some(Giga | Gibi) => self.ui.size.unit_giga,
|
||||
Some(_) => self.ui.size.unit_huge,
|
||||
None => self.ui.size.unit_byte,
|
||||
}
|
||||
}
|
||||
|
||||
fn no_size(&self) -> Style { self.ui.punctuation }
|
||||
fn major(&self) -> Style { self.ui.size.major }
|
||||
fn comma(&self) -> Style { self.ui.punctuation }
|
||||
fn minor(&self) -> Style { self.ui.size.minor }
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl render::UserColours for Theme {
|
||||
fn you(&self) -> Style { self.ui.users.user_you }
|
||||
fn someone_else(&self) -> Style { self.ui.users.user_someone_else }
|
||||
}
|
||||
|
||||
impl FileNameColours for Theme {
|
||||
fn normal_arrow(&self) -> Style { self.ui.punctuation }
|
||||
fn broken_symlink(&self) -> Style { self.ui.broken_symlink }
|
||||
fn broken_filename(&self) -> Style { apply_overlay(self.ui.broken_symlink, self.ui.broken_path_overlay) }
|
||||
fn broken_control_char(&self) -> Style { apply_overlay(self.ui.control_char, self.ui.broken_path_overlay) }
|
||||
fn control_char(&self) -> Style { self.ui.control_char }
|
||||
fn symlink_path(&self) -> Style { self.ui.symlink_path }
|
||||
fn executable_file(&self) -> Style { self.ui.filekinds.executable }
|
||||
|
||||
fn colour_file(&self, file: &File<'_>) -> Style {
|
||||
self.exts.colour_file(file).unwrap_or(self.ui.filekinds.normal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Some of the styles are **overlays**: although they have the same attribute
|
||||
/// set as regular styles (foreground and background colours, bold, underline,
|
||||
/// etc), they’re intended to be used to *amend* existing styles.
|
||||
///
|
||||
/// For example, the target path of a broken symlink is displayed in a red,
|
||||
/// underlined style by default. Paths can contain control characters, so
|
||||
/// these control characters need to be underlined too, otherwise it looks
|
||||
/// weird. So instead of having four separate configurable styles for “link
|
||||
/// path”, “broken link path”, “control character” and “broken control
|
||||
/// character”, there are styles for “link path”, “control character”, and
|
||||
/// “broken link overlay”, the latter of which is just set to override the
|
||||
/// underline attribute on the other two.
|
||||
fn apply_overlay(mut base: Style, overlay: Style) -> Style {
|
||||
if let Some(fg) = overlay.foreground { base.foreground = Some(fg); }
|
||||
if let Some(bg) = overlay.background { base.background = Some(bg); }
|
||||
|
||||
if overlay.is_bold { base.is_bold = true; }
|
||||
if overlay.is_dimmed { base.is_dimmed = true; }
|
||||
if overlay.is_italic { base.is_italic = true; }
|
||||
if overlay.is_underline { base.is_underline = true; }
|
||||
if overlay.is_blink { base.is_blink = true; }
|
||||
if overlay.is_reverse { base.is_reverse = true; }
|
||||
if overlay.is_hidden { base.is_hidden = true; }
|
||||
if overlay.is_strikethrough { base.is_strikethrough = true; }
|
||||
|
||||
base
|
||||
}
|
||||
// TODO: move this function to the ansi_term crate
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod customs_test {
|
||||
use super::*;
|
||||
use crate::theme::ui_styles::UiStyles;
|
||||
use ansi_term::Colour::*;
|
||||
|
||||
macro_rules! test {
|
||||
($name:ident: ls $ls:expr, exa $exa:expr => colours $expected:ident -> $process_expected:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut $expected = UiStyles::default();
|
||||
$process_expected();
|
||||
|
||||
let definitions = Definitions {
|
||||
ls: Some($ls.into()),
|
||||
exa: Some($exa.into()),
|
||||
};
|
||||
|
||||
let mut result = UiStyles::default();
|
||||
let (_exts, _reset) = definitions.parse_color_vars(&mut result);
|
||||
assert_eq!($expected, result);
|
||||
}
|
||||
};
|
||||
($name:ident: ls $ls:expr, exa $exa:expr => exts $mappings:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mappings: Vec<(glob::Pattern, Style)>
|
||||
= $mappings.iter()
|
||||
.map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
|
||||
.collect();
|
||||
|
||||
let definitions = Definitions {
|
||||
ls: Some($ls.into()),
|
||||
exa: Some($exa.into()),
|
||||
};
|
||||
|
||||
let (result, _reset) = definitions.parse_color_vars(&mut UiStyles::default());
|
||||
assert_eq!(ExtensionMappings { mappings }, result);
|
||||
}
|
||||
};
|
||||
($name:ident: ls $ls:expr, exa $exa:expr => colours $expected:ident -> $process_expected:expr, exts $mappings:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut $expected = UiStyles::colourful(false);
|
||||
$process_expected();
|
||||
|
||||
let mappings: Vec<(glob::Pattern, Style)>
|
||||
= $mappings.into_iter()
|
||||
.map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
|
||||
.collect();
|
||||
|
||||
let definitions = Definitions {
|
||||
ls: Some($ls.into()),
|
||||
exa: Some($exa.into()),
|
||||
};
|
||||
|
||||
let mut meh = UiStyles::colourful(false);
|
||||
let (result, _reset) = definitions.parse_color_vars(&vars, &mut meh);
|
||||
assert_eq!(ExtensionMappings { mappings }, result);
|
||||
assert_eq!($expected, meh);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// LS_COLORS can affect all of these colours:
|
||||
test!(ls_di: ls "di=31", exa "" => colours c -> { c.filekinds.directory = Red.normal(); });
|
||||
test!(ls_ex: ls "ex=32", exa "" => colours c -> { c.filekinds.executable = Green.normal(); });
|
||||
test!(ls_fi: ls "fi=33", exa "" => colours c -> { c.filekinds.normal = Yellow.normal(); });
|
||||
test!(ls_pi: ls "pi=34", exa "" => colours c -> { c.filekinds.pipe = Blue.normal(); });
|
||||
test!(ls_so: ls "so=35", exa "" => colours c -> { c.filekinds.socket = Purple.normal(); });
|
||||
test!(ls_bd: ls "bd=36", exa "" => colours c -> { c.filekinds.block_device = Cyan.normal(); });
|
||||
test!(ls_cd: ls "cd=35", exa "" => colours c -> { c.filekinds.char_device = Purple.normal(); });
|
||||
test!(ls_ln: ls "ln=34", exa "" => colours c -> { c.filekinds.symlink = Blue.normal(); });
|
||||
test!(ls_or: ls "or=33", exa "" => colours c -> { c.broken_symlink = Yellow.normal(); });
|
||||
|
||||
// EXA_COLORS can affect all those colours too:
|
||||
test!(exa_di: ls "", exa "di=32" => colours c -> { c.filekinds.directory = Green.normal(); });
|
||||
test!(exa_ex: ls "", exa "ex=33" => colours c -> { c.filekinds.executable = Yellow.normal(); });
|
||||
test!(exa_fi: ls "", exa "fi=34" => colours c -> { c.filekinds.normal = Blue.normal(); });
|
||||
test!(exa_pi: ls "", exa "pi=35" => colours c -> { c.filekinds.pipe = Purple.normal(); });
|
||||
test!(exa_so: ls "", exa "so=36" => colours c -> { c.filekinds.socket = Cyan.normal(); });
|
||||
test!(exa_bd: ls "", exa "bd=35" => colours c -> { c.filekinds.block_device = Purple.normal(); });
|
||||
test!(exa_cd: ls "", exa "cd=34" => colours c -> { c.filekinds.char_device = Blue.normal(); });
|
||||
test!(exa_ln: ls "", exa "ln=33" => colours c -> { c.filekinds.symlink = Yellow.normal(); });
|
||||
test!(exa_or: ls "", exa "or=32" => colours c -> { c.broken_symlink = Green.normal(); });
|
||||
|
||||
// EXA_COLORS will even override options from LS_COLORS:
|
||||
test!(ls_exa_di: ls "di=31", exa "di=32" => colours c -> { c.filekinds.directory = Green.normal(); });
|
||||
test!(ls_exa_ex: ls "ex=32", exa "ex=33" => colours c -> { c.filekinds.executable = Yellow.normal(); });
|
||||
test!(ls_exa_fi: ls "fi=33", exa "fi=34" => colours c -> { c.filekinds.normal = Blue.normal(); });
|
||||
|
||||
// But more importantly, EXA_COLORS has its own, special list of colours:
|
||||
test!(exa_ur: ls "", exa "ur=38;5;100" => colours c -> { c.perms.user_read = Fixed(100).normal(); });
|
||||
test!(exa_uw: ls "", exa "uw=38;5;101" => colours c -> { c.perms.user_write = Fixed(101).normal(); });
|
||||
test!(exa_ux: ls "", exa "ux=38;5;102" => colours c -> { c.perms.user_execute_file = Fixed(102).normal(); });
|
||||
test!(exa_ue: ls "", exa "ue=38;5;103" => colours c -> { c.perms.user_execute_other = Fixed(103).normal(); });
|
||||
test!(exa_gr: ls "", exa "gr=38;5;104" => colours c -> { c.perms.group_read = Fixed(104).normal(); });
|
||||
test!(exa_gw: ls "", exa "gw=38;5;105" => colours c -> { c.perms.group_write = Fixed(105).normal(); });
|
||||
test!(exa_gx: ls "", exa "gx=38;5;106" => colours c -> { c.perms.group_execute = Fixed(106).normal(); });
|
||||
test!(exa_tr: ls "", exa "tr=38;5;107" => colours c -> { c.perms.other_read = Fixed(107).normal(); });
|
||||
test!(exa_tw: ls "", exa "tw=38;5;108" => colours c -> { c.perms.other_write = Fixed(108).normal(); });
|
||||
test!(exa_tx: ls "", exa "tx=38;5;109" => colours c -> { c.perms.other_execute = Fixed(109).normal(); });
|
||||
test!(exa_su: ls "", exa "su=38;5;110" => colours c -> { c.perms.special_user_file = Fixed(110).normal(); });
|
||||
test!(exa_sf: ls "", exa "sf=38;5;111" => colours c -> { c.perms.special_other = Fixed(111).normal(); });
|
||||
test!(exa_xa: ls "", exa "xa=38;5;112" => colours c -> { c.perms.attribute = Fixed(112).normal(); });
|
||||
|
||||
test!(exa_sn: ls "", exa "sn=38;5;113" => colours c -> {
|
||||
c.size.number_byte = Fixed(113).normal();
|
||||
c.size.number_kilo = Fixed(113).normal();
|
||||
c.size.number_mega = Fixed(113).normal();
|
||||
c.size.number_giga = Fixed(113).normal();
|
||||
c.size.number_huge = Fixed(113).normal();
|
||||
});
|
||||
test!(exa_sb: ls "", exa "sb=38;5;114" => colours c -> {
|
||||
c.size.unit_byte = Fixed(114).normal();
|
||||
c.size.unit_kilo = Fixed(114).normal();
|
||||
c.size.unit_mega = Fixed(114).normal();
|
||||
c.size.unit_giga = Fixed(114).normal();
|
||||
c.size.unit_huge = Fixed(114).normal();
|
||||
});
|
||||
|
||||
test!(exa_nb: ls "", exa "nb=38;5;115" => colours c -> { c.size.number_byte = Fixed(115).normal(); });
|
||||
test!(exa_nk: ls "", exa "nk=38;5;116" => colours c -> { c.size.number_kilo = Fixed(116).normal(); });
|
||||
test!(exa_nm: ls "", exa "nm=38;5;117" => colours c -> { c.size.number_mega = Fixed(117).normal(); });
|
||||
test!(exa_ng: ls "", exa "ng=38;5;118" => colours c -> { c.size.number_giga = Fixed(118).normal(); });
|
||||
test!(exa_nh: ls "", exa "nh=38;5;119" => colours c -> { c.size.number_huge = Fixed(119).normal(); });
|
||||
|
||||
test!(exa_ub: ls "", exa "ub=38;5;115" => colours c -> { c.size.unit_byte = Fixed(115).normal(); });
|
||||
test!(exa_uk: ls "", exa "uk=38;5;116" => colours c -> { c.size.unit_kilo = Fixed(116).normal(); });
|
||||
test!(exa_um: ls "", exa "um=38;5;117" => colours c -> { c.size.unit_mega = Fixed(117).normal(); });
|
||||
test!(exa_ug: ls "", exa "ug=38;5;118" => colours c -> { c.size.unit_giga = Fixed(118).normal(); });
|
||||
test!(exa_uh: ls "", exa "uh=38;5;119" => colours c -> { c.size.unit_huge = Fixed(119).normal(); });
|
||||
|
||||
test!(exa_df: ls "", exa "df=38;5;115" => colours c -> { c.size.major = Fixed(115).normal(); });
|
||||
test!(exa_ds: ls "", exa "ds=38;5;116" => colours c -> { c.size.minor = Fixed(116).normal(); });
|
||||
|
||||
test!(exa_uu: ls "", exa "uu=38;5;117" => colours c -> { c.users.user_you = Fixed(117).normal(); });
|
||||
test!(exa_un: ls "", exa "un=38;5;118" => colours c -> { c.users.user_someone_else = Fixed(118).normal(); });
|
||||
test!(exa_gu: ls "", exa "gu=38;5;119" => colours c -> { c.users.group_yours = Fixed(119).normal(); });
|
||||
test!(exa_gn: ls "", exa "gn=38;5;120" => colours c -> { c.users.group_not_yours = Fixed(120).normal(); });
|
||||
|
||||
test!(exa_lc: ls "", exa "lc=38;5;121" => colours c -> { c.links.normal = Fixed(121).normal(); });
|
||||
test!(exa_lm: ls "", exa "lm=38;5;122" => colours c -> { c.links.multi_link_file = Fixed(122).normal(); });
|
||||
|
||||
test!(exa_ga: ls "", exa "ga=38;5;123" => colours c -> { c.git.new = Fixed(123).normal(); });
|
||||
test!(exa_gm: ls "", exa "gm=38;5;124" => colours c -> { c.git.modified = Fixed(124).normal(); });
|
||||
test!(exa_gd: ls "", exa "gd=38;5;125" => colours c -> { c.git.deleted = Fixed(125).normal(); });
|
||||
test!(exa_gv: ls "", exa "gv=38;5;126" => colours c -> { c.git.renamed = Fixed(126).normal(); });
|
||||
test!(exa_gt: ls "", exa "gt=38;5;127" => colours c -> { c.git.typechange = Fixed(127).normal(); });
|
||||
|
||||
test!(exa_xx: ls "", exa "xx=38;5;128" => colours c -> { c.punctuation = Fixed(128).normal(); });
|
||||
test!(exa_da: ls "", exa "da=38;5;129" => colours c -> { c.date = Fixed(129).normal(); });
|
||||
test!(exa_in: ls "", exa "in=38;5;130" => colours c -> { c.inode = Fixed(130).normal(); });
|
||||
test!(exa_bl: ls "", exa "bl=38;5;131" => colours c -> { c.blocks = Fixed(131).normal(); });
|
||||
test!(exa_hd: ls "", exa "hd=38;5;132" => colours c -> { c.header = Fixed(132).normal(); });
|
||||
test!(exa_lp: ls "", exa "lp=38;5;133" => colours c -> { c.symlink_path = Fixed(133).normal(); });
|
||||
test!(exa_cc: ls "", exa "cc=38;5;134" => colours c -> { c.control_char = Fixed(134).normal(); });
|
||||
test!(exa_bo: ls "", exa "bO=4" => colours c -> { c.broken_path_overlay = Style::default().underline(); });
|
||||
|
||||
// All the while, LS_COLORS treats them as filenames:
|
||||
test!(ls_uu: ls "uu=38;5;117", exa "" => exts [ ("uu", Fixed(117).normal()) ]);
|
||||
test!(ls_un: ls "un=38;5;118", exa "" => exts [ ("un", Fixed(118).normal()) ]);
|
||||
test!(ls_gu: ls "gu=38;5;119", exa "" => exts [ ("gu", Fixed(119).normal()) ]);
|
||||
test!(ls_gn: ls "gn=38;5;120", exa "" => exts [ ("gn", Fixed(120).normal()) ]);
|
||||
|
||||
// Just like all other keys:
|
||||
test!(ls_txt: ls "*.txt=31", exa "" => exts [ ("*.txt", Red.normal()) ]);
|
||||
test!(ls_mp3: ls "*.mp3=38;5;135", exa "" => exts [ ("*.mp3", Fixed(135).normal()) ]);
|
||||
test!(ls_mak: ls "Makefile=1;32;4", exa "" => exts [ ("Makefile", Green.bold().underline()) ]);
|
||||
test!(exa_txt: ls "", exa "*.zip=31" => exts [ ("*.zip", Red.normal()) ]);
|
||||
test!(exa_mp3: ls "", exa "lev.*=38;5;153" => exts [ ("lev.*", Fixed(153).normal()) ]);
|
||||
test!(exa_mak: ls "", exa "Cargo.toml=4;32;1" => exts [ ("Cargo.toml", Green.bold().underline()) ]);
|
||||
|
||||
// Testing whether a glob from EXA_COLORS overrides a glob from LS_COLORS
|
||||
// can’t be tested here, because they’ll both be added to the same vec
|
||||
|
||||
// Values get separated by colons:
|
||||
test!(ls_multi: ls "*.txt=31:*.rtf=32", exa "" => exts [ ("*.txt", Red.normal()), ("*.rtf", Green.normal()) ]);
|
||||
test!(exa_multi: ls "", exa "*.tmp=37:*.log=37" => exts [ ("*.tmp", White.normal()), ("*.log", White.normal()) ]);
|
||||
|
||||
test!(ls_five: ls "1*1=31:2*2=32:3*3=1;33:4*4=34;1:5*5=35;4", exa "" => exts [
|
||||
("1*1", Red.normal()), ("2*2", Green.normal()), ("3*3", Yellow.bold()), ("4*4", Blue.bold()), ("5*5", Purple.underline())
|
||||
]);
|
||||
|
||||
// Finally, colours get applied right-to-left:
|
||||
test!(ls_overwrite: ls "pi=31:pi=32:pi=33", exa "" => colours c -> { c.filekinds.pipe = Yellow.normal(); });
|
||||
test!(exa_overwrite: ls "", exa "da=36:da=35:da=34" => colours c -> { c.date = Blue.normal(); });
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
use ansi_term::Style;
|
||||
|
||||
use crate::theme::lsc::Pair;
|
||||
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct UiStyles {
|
||||
pub colourful: bool,
|
||||
|
||||
pub filekinds: FileKinds,
|
||||
pub perms: Permissions,
|
||||
pub size: Size,
|
||||
pub users: Users,
|
||||
pub links: Links,
|
||||
pub git: Git,
|
||||
|
||||
pub punctuation: Style,
|
||||
pub date: Style,
|
||||
pub inode: Style,
|
||||
pub blocks: Style,
|
||||
pub header: Style,
|
||||
pub octal: Style,
|
||||
|
||||
pub symlink_path: Style,
|
||||
pub control_char: Style,
|
||||
pub broken_symlink: Style,
|
||||
pub broken_path_overlay: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct FileKinds {
|
||||
pub normal: Style,
|
||||
pub directory: Style,
|
||||
pub symlink: Style,
|
||||
pub pipe: Style,
|
||||
pub block_device: Style,
|
||||
pub char_device: Style,
|
||||
pub socket: Style,
|
||||
pub special: Style,
|
||||
pub executable: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Permissions {
|
||||
pub user_read: Style,
|
||||
pub user_write: Style,
|
||||
pub user_execute_file: Style,
|
||||
pub user_execute_other: Style,
|
||||
|
||||
pub group_read: Style,
|
||||
pub group_write: Style,
|
||||
pub group_execute: Style,
|
||||
|
||||
pub other_read: Style,
|
||||
pub other_write: Style,
|
||||
pub other_execute: Style,
|
||||
|
||||
pub special_user_file: Style,
|
||||
pub special_other: Style,
|
||||
|
||||
pub attribute: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Size {
|
||||
pub major: Style,
|
||||
pub minor: Style,
|
||||
|
||||
pub number_byte: Style,
|
||||
pub number_kilo: Style,
|
||||
pub number_mega: Style,
|
||||
pub number_giga: Style,
|
||||
pub number_huge: Style,
|
||||
|
||||
pub unit_byte: Style,
|
||||
pub unit_kilo: Style,
|
||||
pub unit_mega: Style,
|
||||
pub unit_giga: Style,
|
||||
pub unit_huge: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Users {
|
||||
pub user_you: Style,
|
||||
pub user_someone_else: Style,
|
||||
pub group_yours: Style,
|
||||
pub group_not_yours: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Links {
|
||||
pub normal: Style,
|
||||
pub multi_link_file: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Git {
|
||||
pub new: Style,
|
||||
pub modified: Style,
|
||||
pub deleted: Style,
|
||||
pub renamed: Style,
|
||||
pub typechange: Style,
|
||||
pub ignored: Style,
|
||||
pub conflicted: Style,
|
||||
}
|
||||
|
||||
impl UiStyles {
|
||||
pub fn plain() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl UiStyles {
|
||||
|
||||
/// Sets a value on this set of colours using one of the keys understood
|
||||
/// by the `LS_COLORS` environment variable. Invalid keys set nothing, but
|
||||
/// return false.
|
||||
pub fn set_ls(&mut self, pair: &Pair<'_>) -> bool {
|
||||
match pair.key {
|
||||
"di" => self.filekinds.directory = pair.to_style(), // DIR
|
||||
"ex" => self.filekinds.executable = pair.to_style(), // EXEC
|
||||
"fi" => self.filekinds.normal = pair.to_style(), // FILE
|
||||
"pi" => self.filekinds.pipe = pair.to_style(), // FIFO
|
||||
"so" => self.filekinds.socket = pair.to_style(), // SOCK
|
||||
"bd" => self.filekinds.block_device = pair.to_style(), // BLK
|
||||
"cd" => self.filekinds.char_device = pair.to_style(), // CHR
|
||||
"ln" => self.filekinds.symlink = pair.to_style(), // LINK
|
||||
"or" => self.broken_symlink = pair.to_style(), // ORPHAN
|
||||
_ => return false,
|
||||
// Codes we don’t do anything with:
|
||||
// MULTIHARDLINK, DOOR, SETUID, SETGID, CAPABILITY,
|
||||
// STICKY_OTHER_WRITABLE, OTHER_WRITABLE, STICKY, MISSING
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Sets a value on this set of colours using one of the keys understood
|
||||
/// by the `EXA_COLORS` environment variable. Invalid keys set nothing,
|
||||
/// but return false. This doesn’t take the `LS_COLORS` keys into account,
|
||||
/// so `set_ls` should have been run first.
|
||||
pub fn set_exa(&mut self, pair: &Pair<'_>) -> bool {
|
||||
match pair.key {
|
||||
"ur" => self.perms.user_read = pair.to_style(),
|
||||
"uw" => self.perms.user_write = pair.to_style(),
|
||||
"ux" => self.perms.user_execute_file = pair.to_style(),
|
||||
"ue" => self.perms.user_execute_other = pair.to_style(),
|
||||
"gr" => self.perms.group_read = pair.to_style(),
|
||||
"gw" => self.perms.group_write = pair.to_style(),
|
||||
"gx" => self.perms.group_execute = pair.to_style(),
|
||||
"tr" => self.perms.other_read = pair.to_style(),
|
||||
"tw" => self.perms.other_write = pair.to_style(),
|
||||
"tx" => self.perms.other_execute = pair.to_style(),
|
||||
"su" => self.perms.special_user_file = pair.to_style(),
|
||||
"sf" => self.perms.special_other = pair.to_style(),
|
||||
"xa" => self.perms.attribute = pair.to_style(),
|
||||
|
||||
"sn" => self.set_number_style(pair.to_style()),
|
||||
"sb" => self.set_unit_style(pair.to_style()),
|
||||
"nb" => self.size.number_byte = pair.to_style(),
|
||||
"nk" => self.size.number_kilo = pair.to_style(),
|
||||
"nm" => self.size.number_mega = pair.to_style(),
|
||||
"ng" => self.size.number_giga = pair.to_style(),
|
||||
"nh" => self.size.number_huge = pair.to_style(),
|
||||
"ub" => self.size.unit_byte = pair.to_style(),
|
||||
"uk" => self.size.unit_kilo = pair.to_style(),
|
||||
"um" => self.size.unit_mega = pair.to_style(),
|
||||
"ug" => self.size.unit_giga = pair.to_style(),
|
||||
"uh" => self.size.unit_huge = pair.to_style(),
|
||||
"df" => self.size.major = pair.to_style(),
|
||||
"ds" => self.size.minor = pair.to_style(),
|
||||
|
||||
"uu" => self.users.user_you = pair.to_style(),
|
||||
"un" => self.users.user_someone_else = pair.to_style(),
|
||||
"gu" => self.users.group_yours = pair.to_style(),
|
||||
"gn" => self.users.group_not_yours = pair.to_style(),
|
||||
|
||||
"lc" => self.links.normal = pair.to_style(),
|
||||
"lm" => self.links.multi_link_file = pair.to_style(),
|
||||
|
||||
"ga" => self.git.new = pair.to_style(),
|
||||
"gm" => self.git.modified = pair.to_style(),
|
||||
"gd" => self.git.deleted = pair.to_style(),
|
||||
"gv" => self.git.renamed = pair.to_style(),
|
||||
"gt" => self.git.typechange = pair.to_style(),
|
||||
|
||||
"xx" => self.punctuation = pair.to_style(),
|
||||
"da" => self.date = pair.to_style(),
|
||||
"in" => self.inode = pair.to_style(),
|
||||
"bl" => self.blocks = pair.to_style(),
|
||||
"hd" => self.header = pair.to_style(),
|
||||
"lp" => self.symlink_path = pair.to_style(),
|
||||
"cc" => self.control_char = pair.to_style(),
|
||||
"bO" => self.broken_path_overlay = pair.to_style(),
|
||||
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_number_style(&mut self, style: Style) {
|
||||
self.size.number_byte = style;
|
||||
self.size.number_kilo = style;
|
||||
self.size.number_mega = style;
|
||||
self.size.number_giga = style;
|
||||
self.size.number_huge = style;
|
||||
}
|
||||
|
||||
pub fn set_unit_style(&mut self, style: Style) {
|
||||
self.size.unit_byte = style;
|
||||
self.size.unit_kilo = style;
|
||||
self.size.unit_mega = style;
|
||||
self.size.unit_giga = style;
|
||||
self.size.unit_huge = style;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,47 @@
|
|||
## Extra tests
|
||||
# exa › xtests
|
||||
|
||||
These extra tests are intended to be run from a Vagrant VM that has already had its environment set up -- see the section in the README for more details.
|
||||
These are the **extended tests**. They are integration tests: they run the `exa` binary with select configurations of parameters and environment variables, and assert that the program prints the correct text to standard output and error, and exits with the correct status code.
|
||||
|
||||
They test things like:
|
||||
|
||||
- broken symlinks
|
||||
- extended attributes
|
||||
- file names with weird stuff like newlines or escapes in
|
||||
- invalid UTF-8
|
||||
- missing users and groups
|
||||
- nested Git repositories
|
||||
|
||||
They are intended to be run from the Vagrant VM that has already had its environment set up — see the `devtools/dev-create-test-filesystem.sh` script for how the files are generated.
|
||||
|
||||
|
||||
## Anatomy of the tests
|
||||
|
||||
The tests are run using [Specsheet](https://specsheet.software/). The TOML files define the tests, and the files in `output/` contain the output that exa should produce.
|
||||
|
||||
For example, let’s look at one of the tests in `lines-view.toml`. This test checks that running exa does the right thing when running with the `-1` argument, and a directory full of files:
|
||||
|
||||
```toml
|
||||
[[cmd]]
|
||||
name = "‘exa -1’ displays file names, one on each line"
|
||||
shell = "exa -1 /testcases/file-names"
|
||||
stdout = { file = "outputs/names_lines.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'oneline' ]
|
||||
```
|
||||
|
||||
Here’s an explanation of each line:
|
||||
|
||||
1. The `[[cmd]]` line marks this test as a [cmd](https://specsheet.software/checks/command/cmd) check, which can run arbitrary commands. In this case, the commad is exa with some arguments.
|
||||
|
||||
2. The `name` field is a human-readable description of the feature of exa that’s under test. It gets printed to the screen as tests are run.
|
||||
|
||||
3. The `shell` field contains the shell script to execute. It should have `exa` in there somewhere.
|
||||
|
||||
4. The `stdout` field describes the [content](https://specsheet.software/docs/check-file-schema#content) that exa should print to standard output. In this case, the test asserts that the output of running the program should be identical to the contents of the file.
|
||||
|
||||
5. The `stderr` field describes the content of standard error. In this case, it asserts that nothing is printed to stderr.
|
||||
|
||||
6. The `status` field asserts that exa should exit with a status code of 0.
|
||||
|
||||
7. The `tags` field does not change the test at all, but can be used to filter which tests are run, instead of running all of them each time.
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
[[cmd]]
|
||||
name = "‘exa -@lT’ produces a tree view with metadata and attribute entries"
|
||||
shell = "exa -@lT /testcases/attributes"
|
||||
stdout = { file = "outputs/attributes_xattrs_long_tree.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'xattrs', 'long', 'tree' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -@T’ produces a tree view with attribute entries"
|
||||
shell = "exa -@T /testcases/attributes"
|
||||
stdout = { file = "outputs/attributes_xattrs_tree.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'xattrs', 'tree' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -@T’ with file arguments produces a tree view with attribute entries"
|
||||
shell = "exa -@T /testcases/attributes/*"
|
||||
stdout = { file = "outputs/attributes_files_xattrs_tree.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'xattrs', 'tree' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -@T’ produces a tree view with attribute entries of symlinks"
|
||||
shell = "exa -@T /testcases/links"
|
||||
stdout = { file = "outputs/links_xattrs_tree.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'xattrs', 'tree' ]
|
||||
|
||||
|
||||
# permission errors tests
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -@T’ displays an inaccessible directory with errors"
|
||||
shell = "exa -@T /proc/1/root"
|
||||
stdout = { file = "outputs/proc_1_root_xattrs.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'tree' ]
|
|
@ -0,0 +1,57 @@
|
|||
# details view (check the argument works)
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l --colour=always’ always uses colours for metadata"
|
||||
shell = "exa -l --colour=always /testcases/files"
|
||||
stdout = { file = "outputs/files_long.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'colour-term' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l --colour=never’ never uses colours for metadata"
|
||||
shell = "exa -l --colour=never /testcases/files"
|
||||
stdout = { file = "outputs/files_long_monochrome.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'colour-term' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l --colour=automatic’ uses colours dependently for metadata"
|
||||
shell = "exa -l --colour=automatic /testcases/files"
|
||||
stdout = { file = "outputs/files_long_monochrome.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'colour-term' ]
|
||||
|
||||
|
||||
# grid view (check that all colours are turned off)
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa --colour=never’ never uses colours for file names"
|
||||
shell = "exa --colour=never /testcases/file-names"
|
||||
environment = { COLUMNS = "80" }
|
||||
stdout = { file = "outputs/files_grid_monochrome.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'grid', 'colour-term' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa --colour=never’ never uses colours for files based on their extensions"
|
||||
shell = "exa --colour=never /testcases/file-names-exts"
|
||||
environment = { COLUMNS = "80" }
|
||||
stdout = { file = "outputs/exts_grid_monochrome.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'grid', 'colour-term' ]
|
||||
|
||||
|
||||
# tree view (check that all colours are turned off)
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -T --colour=never’ never uses colours for punctuation and symlink targets"
|
||||
shell = "exa -T --colour=never /testcases/file-names/links"
|
||||
stdout = { file = "outputs/links_grid_monochrome.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'tree', 'colour-term' ]
|
|
@ -1,4 +0,0 @@
|
|||
[4mPermissions[0m [4mSize[0m [4mUser[0m [4mDate Modified[0m [4mName[0m
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 3 Mar 2003[0m pear
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m15 Jun 2006[0m peach
|
||||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m22 Dec 2009[0m plum
|
|
@ -0,0 +1,17 @@
|
|||
[[cmd]]
|
||||
name = "‘EXA_DEBUG=1 exa’ produces debug output"
|
||||
shell = "exa --long /testcases"
|
||||
environment = { EXA_DEBUG = "1" }
|
||||
stdout = { empty = false }
|
||||
stderr = { string = "DEBUG" }
|
||||
status = 0
|
||||
tags = [ 'debug', 'env', 'long' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘EXA_DEBUG=trace exa’ produces trace-level debug output"
|
||||
shell = "exa --long /testcases"
|
||||
environment = { EXA_DEBUG = "trace" }
|
||||
stdout = { empty = false }
|
||||
stderr = { string = "TRACE" }
|
||||
status = 0
|
||||
tags = [ 'debug', 'env', 'long' ]
|
|
@ -0,0 +1,93 @@
|
|||
# various date fields
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -lh’ produces a table using the modified time field"
|
||||
shell = "exa -lh /testcases/dates"
|
||||
stdout = { file = "outputs/dates_long_time_modified.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'header', 'time' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -lh --time=modified’ produces a table using the modified time field"
|
||||
shell = "exa -lh --time=modified /testcases/dates"
|
||||
stdout = { file = "outputs/dates_long_time_modified.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'header', 'time' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -lh --time=accessed’ produces a table using the accessed time field"
|
||||
shell = "exa -lh --time=accessed /testcases/dates"
|
||||
stdout = { file = "outputs/dates_long_time_accessed.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'header', 'time' ]
|
||||
|
||||
|
||||
# distant past and far future dates
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l’ handles dates far past and future dates"
|
||||
shell = "exa -l /testcases/far-dates"
|
||||
stdout = { file = "outputs/far_dates_long.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'time' ]
|
||||
|
||||
|
||||
# alternate date formats
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l --time-style=long-iso’ produces a table using the long-iso date format"
|
||||
shell = "exa -l --time-style=long-iso /testcases/dates"
|
||||
stdout = { file = "outputs/dates_long_timestyle_longiso.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'time-style' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l --time-style=full-iso’ produces a table using the full-iso date format"
|
||||
shell = "exa -l --time-style=full-iso /testcases/dates"
|
||||
stdout = { file = "outputs/dates_long_timestyle_fulliso.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'time-style' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l --time-style=iso’ produces a table using the iso date format"
|
||||
shell = "exa -l --time-style=iso /testcases/dates"
|
||||
stdout = { file = "outputs/dates_long_timestyle_iso.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'time-style' ]
|
||||
|
||||
|
||||
# locales
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l’ using a locale with 4-character-long month abbreviations (‘ja_JP’) sizes the date column correctly"
|
||||
shell = "exa -l /testcases/dates"
|
||||
environment = { LC_TIME = "ja_JP.UTF-8", LANG = "ja_JP.UTF-8" }
|
||||
stdout = { file = "outputs/dates_long_localejp.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'locales' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l’ using a locale with 5-character-long month abbreviations (‘fr_FR’) sizes the date column correctly"
|
||||
shell = "exa -l /testcases/dates"
|
||||
environment = { LC_TIME = "fr_FR.UTF-8", LANG = "fr_FR.UTF-8" }
|
||||
stdout = { file = "outputs/dates_long_localefr.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'locales' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l’ using a locale (‘fr_FR’) display dates of the current year with localized month name"
|
||||
shell = "exa -l /testcases/files"
|
||||
environment = { LC_TIME = "fr_FR.UTF-8", LANG = "fr_FR.UTF-8" }
|
||||
stdout = { file = "outputs/dates_long_currentyear_localefr.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'locales' ]
|
|
@ -0,0 +1,74 @@
|
|||
[[cmd]]
|
||||
name = "‘exa -lb’ produces a details table with binary file sizes"
|
||||
shell = "exa -lb /testcases/files"
|
||||
stdout = { file = "outputs/files_long_binary.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'binary' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -lB’ produces a details table with bytes file sizes"
|
||||
shell = "exa -lB /testcases/files"
|
||||
stdout = { file = "outputs/files_long_bytes.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'bytes' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -lhb’ produces a details table with a header and binary file sizes"
|
||||
shell = "exa -lhb /testcases/files"
|
||||
stdout = { file = "outputs/files_long_header_binary.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'header', 'binary' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -lhB’ produces a details table with a header and bytes file sizes"
|
||||
shell = "exa -lhB /testcases/files"
|
||||
stdout = { file = "outputs/files_long_header_bytes.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'header', 'bytes' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l --color-scale’ (US spelling) produces a details table using a file size colour scale"
|
||||
shell = "exa -l --color-scale /testcases/files"
|
||||
stdout = { file = "outputs/files_long_colourscale.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'colour-scale' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l --colour-scale’ (UK spelling) produces a details table using a file size colour scale"
|
||||
shell = "exa -l --colour-scale /testcases/files"
|
||||
stdout = { file = "outputs/files_long_colourscale.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'colour-scale' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l --colour-scale --binary’ produces a details table using a file size colour scale and binary sizes"
|
||||
shell = "exa -l --colour-scale --binary /testcases/files"
|
||||
stdout = { file = "outputs/files_long_colourscale_binary.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'colour-scale', 'binary' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l --colour-scale --bytes’ produces a details table using a file size colour scale and byte sizes"
|
||||
shell = "exa -l --colour-scale --bytes /testcases/files"
|
||||
stdout = { file = "outputs/files_long_colourscale_bytes.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'colour-scale', 'bytes' ]
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa -l’ produces a details table with major and minor device IDs"
|
||||
shell = "cd /dev; exa -l mem null port zero full random urandom --sort=none --no-time"
|
||||
stdout = { file = "outputs/dev_long.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'dev' ]
|
||||
|
||||
# these particular device IDs should be fixed:
|
||||
# https://raw.githubusercontent.com/torvalds/linux/master/Documentation/admin-guide/devices.txt
|
|
@ -0,0 +1,7 @@
|
|||
[[cmd]]
|
||||
name = "‘exa -lgh’ produces a tree view with attribute entries"
|
||||
shell = "exa -lgh /testcases/passwd"
|
||||
stdout = { file = "outputs/passwd_long_group_header.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'group', 'header' ]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue