mirror of https://github.com/Llewellynvdm/exa.git
Compare commits
170 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 | |
skyline75489 | 0adc5c789b | |
skyline75489 | 8ad46e2ee5 | |
Chester Liu | 0ea8f17b22 | |
Chester Liu | 00f97a9738 | |
Chester Liu | 78a3bc9838 | |
Chester Liu | 9d613016c0 | |
Chester Liu | e874584a55 | |
Chester Liu | 33dd8fd2ca | |
Chester Liu | 13b3635407 | |
Chester Liu | 5503e4756e | |
Chester Liu | 31583691d5 | |
Chester Liu | aeb4a679e8 | |
Chester Liu | e9d0af0343 | |
Chester Liu | 0e8a4582d0 | |
Chester Liu | 6a642d0f32 | |
Kat Marchán | 7f717c3af3 |
|
@ -1,14 +1,8 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report a crash, runtime error, or invalid output in exa
|
||||
name: exa is unmaintained
|
||||
about: Please use the active fork eza instead. <https://github.com/eza-community/eza>
|
||||
---
|
||||
|
||||
If exa does something unexpected, or its output looks wrong, or it displays an error on the screen, or if it outright crashes, then please include the following information in your report:
|
||||
|
||||
- The version of exa being used (`exa --version`)
|
||||
- The command-line arguments you are using
|
||||
- Your operating system and hardware platform
|
||||
|
||||
If it’s a crash, please include the full text of the crash that gets printed to the screen. If you’re seeing unexpected behaviour, a screenshot of the issue will help a lot.
|
||||
exa is unmaintained, please use the active fork eza instead. <https://github.com/eza-community/eza>
|
||||
|
||||
---
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
name: Compilation error
|
||||
about: Report a problem compiling exa
|
||||
---
|
||||
|
||||
If exa fails to compile, or if there is a problem during the build process, then please include the following information in your report:
|
||||
|
||||
- The exact exa commit you are building (`git rev-parse --short HEAD`)
|
||||
- The version of rustc you are compiling it with (`rustc --version`)
|
||||
- Your operating system and hardware platform
|
||||
- The Rust build target (the _exact_ output of `rustc --print cfg`)
|
||||
|
||||
If you are seeing compilation errors, please include the output of the build process.
|
||||
|
||||
---
|
|
@ -1 +0,0 @@
|
|||
blank_issues_enabled: true
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Request a feature or enhancement to exa
|
||||
---
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
name: Question
|
||||
about: Ask a question about exa
|
||||
---
|
|
@ -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)
|
||||
|
|
19
.travis.yml
19
.travis.yml
|
@ -1,19 +0,0 @@
|
|||
language: rust
|
||||
rust:
|
||||
- 1.42.0
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
jobs:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
|
||||
include:
|
||||
- name: 'Rust: test with all features'
|
||||
rust: stable
|
||||
install:
|
||||
- cargo install cargo-hack
|
||||
script:
|
||||
- cargo hack test --feature-powerset
|
|
@ -1,5 +1,7 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
|
@ -57,7 +59,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "exa"
|
||||
version = "0.11.0-pre"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"datetime",
|
||||
|
@ -72,7 +74,7 @@ dependencies = [
|
|||
"number_prefix",
|
||||
"scoped_threadpool",
|
||||
"term_grid",
|
||||
"term_size",
|
||||
"terminal_size",
|
||||
"unicode-width",
|
||||
"users",
|
||||
"zoneinfo_compiled",
|
||||
|
@ -90,9 +92,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.13.17"
|
||||
version = "0.13.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d250f5f82326884bd39c2853577e70a121775db76818ffa452ed1e80de12986"
|
||||
checksum = "d9831e983241f8c5591ed53f17d874833e2fa82cac2625f3888c50cbfe136cba"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
|
@ -119,9 +121,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21"
|
||||
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"unicode-bidi",
|
||||
|
@ -130,9 +132,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.21"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
|
||||
checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -151,9 +153,9 @@ checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
|
|||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.12.18+1.1.0"
|
||||
version = "0.12.21+1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3da6a42da88fc37ee1ecda212ffa254c25713532980005d5f7c0b0fbe7e6e885"
|
||||
checksum = "86271bacd72b2b9e854c3dcfb82efd538f15f870e4c11af66900effb462f6825"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
|
@ -277,18 +279,18 @@ 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 = "230d3e804faaed5a39b08319efb797783df2fd9671b39b7596490cb486d702cf"
|
||||
checksum = "a7c9eb7705cb3f0fd71d3955b23db6d372142ac139e8c473952c93bf3c3dc4b7"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term_size"
|
||||
version = "0.3.2"
|
||||
name = "terminal_size"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9"
|
||||
checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
|
@ -357,9 +359,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.11"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
|
||||
checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
|
|
22
Cargo.toml
22
Cargo.toml
|
@ -1,11 +1,12 @@
|
|||
[package]
|
||||
name = "exa"
|
||||
description = "A modern replacement for ls"
|
||||
|
||||
authors = ["Benjamin Sago <ogham@bsago.me>"]
|
||||
categories = ["command-line-utilities"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
rust-version = "1.66.1"
|
||||
exclude = ["/devtools/*", "/Justfile", "/Vagrantfile", "/screenshots.png"]
|
||||
readme = "README.md"
|
||||
homepage = "https://the.exa.website/"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/ogham/exa"
|
||||
|
@ -27,12 +28,14 @@ natord = "1.0"
|
|||
num_cpus = "1.10"
|
||||
number_prefix = "0.4"
|
||||
scoped_threadpool = "0.1"
|
||||
term_grid = "0.1"
|
||||
term_size = "0.3"
|
||||
term_grid = "0.2.0"
|
||||
terminal_size = "0.1.16"
|
||||
unicode-width = "0.1"
|
||||
users = "0.11"
|
||||
zoneinfo_compiled = "0.5.1"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
users = "0.11"
|
||||
|
||||
[dependencies.datetime]
|
||||
version = "0.5.2"
|
||||
default-features = false
|
||||
|
@ -63,7 +66,7 @@ lto = true
|
|||
|
||||
|
||||
[package.metadata.deb]
|
||||
license-file = [ "LICENCE" ]
|
||||
license-file = [ "LICENCE", "4" ]
|
||||
depends = "$auto"
|
||||
extended-description = """
|
||||
exa is a replacement for ls written in Rust.
|
||||
|
@ -72,6 +75,9 @@ section = "utils"
|
|||
priority = "optional"
|
||||
assets = [
|
||||
[ "target/release/exa", "/usr/bin/exa", "0755" ],
|
||||
[ "contrib/man/exa.1", "/usr/share/man/man1/exa.1", "0644" ],
|
||||
[ "contrib/completions.bash", "/etc/bash_completion.d/exa", "0644" ],
|
||||
[ "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" ],
|
||||
]
|
||||
|
|
61
README.md
61
README.md
|
@ -1,17 +1,19 @@
|
|||
# exa is unmaintained, use the [fork eza](https://github.com/eza-community/eza) instead.
|
||||
|
||||
(This repository isn’t archived because the only person with the rights to do so is unreachable).
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<h1>exa</h1>
|
||||
|
||||
# exa
|
||||
|
||||
[exa](https://the.exa.website/) is a modern replacement for _ls_.
|
||||
|
||||
**README Sections:** [Options](#options) — [Installation](#installation) — [Development](#development)
|
||||
|
||||
<a href="https://travis-ci.org/github/ogham/exa">
|
||||
<img src="https://travis-ci.org/ogham/exa.svg?branch=master" alt="Build status" />
|
||||
</a>
|
||||
|
||||
<a href="https://saythanks.io/to/ogham%40bsago.me">
|
||||
<img src="https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg" alt="Say thanks!" />
|
||||
</a>
|
||||
[![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)
|
||||
|
@ -109,74 +111,73 @@ More information on how to install exa is available on [the Installation page](h
|
|||
|
||||
On Alpine Linux, [enable community repository](https://wiki.alpinelinux.org/wiki/Enable_Community_Repository) and install the [`exa`](https://pkgs.alpinelinux.org/package/edge/community/x86_64/exa) package.
|
||||
|
||||
$ apk add exa
|
||||
apk add exa
|
||||
|
||||
### Arch Linux
|
||||
|
||||
On Arch, install the [`exa`](https://www.archlinux.org/packages/community/x86_64/exa/) package.
|
||||
|
||||
$ pacman -S exa
|
||||
pacman -S exa
|
||||
|
||||
### Android / Termux
|
||||
|
||||
On Android / Termux, install the [`exa`](https://github.com/termux/termux-packages/tree/master/packages/exa) package.
|
||||
|
||||
$ pkg install exa
|
||||
pkg install exa
|
||||
|
||||
### Debian
|
||||
|
||||
On Debian, install the [`exa`](https://packages.debian.org/unstable/exa) package.
|
||||
For now, exa is in the _unstable_ repository.
|
||||
On Debian, install the [`exa`](https://packages.debian.org/stable/exa) package.
|
||||
|
||||
$ apt install exa
|
||||
apt install exa
|
||||
|
||||
### Fedora
|
||||
|
||||
On Fedora, install the [`exa`](https://src.fedoraproject.org/modules/exa) package.
|
||||
|
||||
$ dnf install exa
|
||||
dnf install exa
|
||||
|
||||
### Gentoo
|
||||
|
||||
On Gentoo, install the [`sys-apps/exa`](https://packages.gentoo.org/packages/sys-apps/exa) package.
|
||||
|
||||
$ emerge sys-apps/exa
|
||||
emerge sys-apps/exa
|
||||
|
||||
### Homebrew
|
||||
|
||||
If you’re using [Homebrew](https://brew.sh/) on macOS, install the [`exa`](http://formulae.brew.sh/formula/exa) formula.
|
||||
|
||||
$ brew install exa
|
||||
brew install exa
|
||||
|
||||
### MacPorts
|
||||
|
||||
If you're using [MacPorts](https://www.macports.org/) on macOS, install the [`exa`](https://ports.macports.org/port/exa/summary) port.
|
||||
|
||||
$ port install exa
|
||||
port install exa
|
||||
|
||||
### Nix
|
||||
|
||||
On nixOS, install the [`exa`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/exa/default.nix) package.
|
||||
|
||||
$ nix-env -i exa
|
||||
nix-env -i exa
|
||||
|
||||
### openSUSE
|
||||
|
||||
On openSUSE, install the [`exa`](https://software.opensuse.org/package/exa) package.
|
||||
|
||||
$ zypper install exa
|
||||
zypper install exa
|
||||
|
||||
### Ubuntu
|
||||
|
||||
On Ubuntu 20.10 (Groovy Gorilla) and later, install the [`exa`](https://packages.ubuntu.com/groovy/exa) package.
|
||||
On Ubuntu 20.10 (Groovy Gorilla) and later, install the [`exa`](https://packages.ubuntu.com/jammy/exa) package.
|
||||
|
||||
$ sudo apt install exa
|
||||
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
|
||||
xbps-install -S exa
|
||||
|
||||
### Manual installation from GitHub
|
||||
|
||||
|
@ -189,7 +190,7 @@ For more information, see the [Manual Installation page](https://the.exa.website
|
|||
|
||||
If you already have a Rust environment set up, you can use the `cargo install` command:
|
||||
|
||||
$ cargo install exa
|
||||
cargo install exa
|
||||
|
||||
Cargo will build the `exa` binary and place it in `$HOME/.cargo`.
|
||||
|
||||
|
@ -201,8 +202,8 @@ To build without Git support, run `cargo install --no-default-features exa` is a
|
|||
<a id="development">
|
||||
<h1>Development
|
||||
|
||||
<a href="https://blog.rust-lang.org/2020/03/12/Rust-1.42.html">
|
||||
<img src="https://img.shields.io/badge/rustc-1.42+-lightgray.svg" alt="Rust 1.42+" />
|
||||
<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">
|
||||
|
@ -211,16 +212,16 @@ To build without Git support, run `cargo install --no-default-features exa` is a
|
|||
</h1></a>
|
||||
|
||||
exa is written in [Rust](https://www.rust-lang.org/).
|
||||
You will need rustc version 1.42.0 or higher.
|
||||
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
|
||||
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 --tasks` to get an overview of what’s available.
|
||||
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`.
|
||||
|
|
3
build.rs
3
build.rs
|
@ -38,9 +38,10 @@ fn main() -> io::Result<()> {
|
|||
|
||||
// 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(&out.join("version_string.txt"))?;
|
||||
let mut f = File::create(path).unwrap_or_else(|_| { panic!("{}", path.to_string_lossy().to_string()) });
|
||||
writeln!(f, "{}", strip_codes(&ver))?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -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,10 +10,14 @@ 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"
|
||||
|
||||
|
@ -22,9 +26,9 @@ complete -c exa -l 'group-directories-first' -d "Sort directories before other f
|
|||
complete -c exa -l 'git-ignore' -d "Ignore files mentioned in '.gitignore'"
|
||||
complete -c exa -s 'a' -l 'all' -d "Show hidden and 'dot' files"
|
||||
complete -c exa -s 'd' -l 'list-dirs' -d "List directories like regular files"
|
||||
complete -c exa -s 'L' -l 'level' -d "Limit the depth of recursion" -a "1 2 3 4 5 6 7 8 9"
|
||||
complete -c exa -s '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'
|
||||
|
@ -56,10 +60,10 @@ 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'
|
||||
|
@ -70,7 +74,7 @@ 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'
|
|
@ -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,7 +19,7 @@ __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]" \
|
|
@ -11,17 +11,17 @@ 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() { [ "$EXA_DEBUG" == "trace" ] && echo -n "trace-"; [ -n "$EXA_DEBUG" ] && echo "debug "; }
|
||||
function strict_mode() { [ -n "$EXA_STRICT" ] && echo "strict "; }
|
||||
function lsc_mode() { [ -n "$LS_COLORS" ] && echo "lsc "; }
|
||||
function exac_mode() { [ -n "$EXA_COLORS" ] && echo "exac "; }
|
||||
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 () {
|
||||
debug() {
|
||||
case "$1" in
|
||||
""|"on") export EXA_DEBUG=1 ;;
|
||||
"off") export EXA_DEBUG= ;;
|
||||
|
@ -33,11 +33,12 @@ function debug () {
|
|||
|
||||
# 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 ;;
|
||||
"") [ -n "$EXA_STRICT" ] && echo "strict on" || echo "strict off" ;;
|
||||
*) echo "Usage: strict on|off"; return 1 ;;
|
||||
esac;
|
||||
}
|
||||
|
||||
|
@ -45,7 +46,7 @@ function strict () {
|
|||
# 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"
|
||||
|
|
|
@ -252,7 +252,7 @@ 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/3)"
|
||||
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
|
||||
|
@ -281,7 +281,7 @@ 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/3)"
|
||||
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
|
||||
|
@ -321,7 +321,7 @@ 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/3)"
|
||||
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
|
||||
|
@ -334,6 +334,20 @@ 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.
|
||||
|
|
|
@ -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.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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -224,6 +224,12 @@ Specifies the number of spaces to print between an icon (see the ‘`--icons`’
|
|||
|
||||
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.
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "1.66.1"
|
|
@ -111,6 +111,13 @@ impl<'dir, 'ig> Files<'dir, 'ig> {
|
|||
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 {
|
||||
|
@ -121,9 +128,8 @@ impl<'dir, 'ig> Files<'dir, 'ig> {
|
|||
return Some(File::from_args(path.clone(), self.dir, filename)
|
||||
.map_err(|e| (path.clone(), e)))
|
||||
}
|
||||
else {
|
||||
return None
|
||||
}
|
||||
|
||||
return None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +176,7 @@ impl<'dir, 'ig> Iterator for Files<'dir, 'ig> {
|
|||
/// 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
|
||||
/// 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 `..`.
|
||||
|
|
|
@ -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
|
||||
|
@ -51,14 +51,14 @@ impl DirAction {
|
|||
match self {
|
||||
Self::AsFile => true,
|
||||
Self::Recurse(o) => o.tree,
|
||||
_ => false,
|
||||
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
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
//! 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;
|
||||
|
||||
|
@ -205,6 +208,11 @@ 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);
|
||||
|
@ -288,6 +296,7 @@ impl Git {
|
|||
/// Paths need to be absolute for them to be compared properly, otherwise
|
||||
/// you’d ask a repo about “./README.md” but it only knows about
|
||||
/// “/vagrant/README.md”, prefixed by the workdir.
|
||||
#[cfg(unix)]
|
||||
fn reorient(path: &Path) -> PathBuf {
|
||||
use std::env::current_dir;
|
||||
|
||||
|
@ -300,6 +309,14 @@ fn reorient(path: &Path) -> PathBuf {
|
|||
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.
|
||||
fn working_tree_status(status: git2::Status) -> f::GitStatus {
|
||||
match status {
|
||||
|
|
|
@ -167,7 +167,7 @@ mod lister {
|
|||
unsafe {
|
||||
listxattr(
|
||||
c_path.as_ptr(),
|
||||
buf.as_mut_ptr() as *mut c_char,
|
||||
buf.as_mut_ptr().cast::<c_char>(),
|
||||
bufsize as size_t,
|
||||
self.c_flags,
|
||||
)
|
||||
|
@ -178,7 +178,7 @@ mod lister {
|
|||
unsafe {
|
||||
getxattr(
|
||||
c_path.as_ptr(),
|
||||
buf.as_ptr() as *const c_char,
|
||||
buf.as_ptr().cast::<c_char>(),
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
0,
|
||||
|
@ -246,7 +246,7 @@ mod lister {
|
|||
|
||||
unsafe {
|
||||
listxattr(
|
||||
c_path.as_ptr() as *const _,
|
||||
c_path.as_ptr().cast(),
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
|
@ -261,8 +261,8 @@ mod lister {
|
|||
|
||||
unsafe {
|
||||
listxattr(
|
||||
c_path.as_ptr() as *const _,
|
||||
buf.as_mut_ptr() as *mut c_char,
|
||||
c_path.as_ptr().cast(),
|
||||
buf.as_mut_ptr().cast(),
|
||||
bufsize as size_t,
|
||||
)
|
||||
}
|
||||
|
@ -276,8 +276,8 @@ mod lister {
|
|||
|
||||
unsafe {
|
||||
getxattr(
|
||||
c_path.as_ptr() as *const _,
|
||||
buf.as_ptr() as *const c_char,
|
||||
c_path.as_ptr().cast(),
|
||||
buf.as_ptr().cast(),
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
|
|
|
@ -82,13 +82,27 @@ 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,
|
||||
}
|
||||
|
||||
|
@ -162,7 +176,7 @@ pub enum Size {
|
|||
/// 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
|
||||
|
@ -196,7 +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, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
pub enum GitStatus {
|
||||
|
||||
/// This file hasn’t changed since the last commit.
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
//! Files, and methods and fields to access their metadata.
|
||||
|
||||
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::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
|
@ -78,11 +81,11 @@ impl<'dir> File<'dir> {
|
|||
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) -> io::Result<File<'dir>> {
|
||||
let path = parent_dir.path.to_path_buf();
|
||||
let path = parent_dir.path.clone();
|
||||
let ext = File::ext(&path);
|
||||
|
||||
debug!("Statting file {:?}", &path);
|
||||
|
@ -174,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
|
||||
|
@ -185,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()
|
||||
}
|
||||
|
@ -213,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,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();
|
||||
|
||||
|
@ -280,6 +289,7 @@ impl<'dir> File<'dir> {
|
|||
}
|
||||
|
||||
/// This file’s inode.
|
||||
#[cfg(unix)]
|
||||
pub fn inode(&self) -> f::Inode {
|
||||
f::Inode(self.metadata.ino())
|
||||
}
|
||||
|
@ -287,6 +297,7 @@ impl<'dir> File<'dir> {
|
|||
/// This file’s number of filesystem blocks.
|
||||
///
|
||||
/// (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())
|
||||
|
@ -297,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())
|
||||
}
|
||||
|
@ -314,6 +327,7 @@ 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
|
||||
|
@ -335,12 +349,23 @@ impl<'dir> File<'dir> {
|
|||
}
|
||||
}
|
||||
|
||||
#[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 modified timestamp, if available on this platform.
|
||||
pub fn modified_time(&self) -> Option<SystemTime> {
|
||||
self.metadata.modified().ok()
|
||||
}
|
||||
|
||||
/// 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());
|
||||
|
||||
|
@ -350,7 +375,7 @@ impl<'dir> File<'dir> {
|
|||
nanosec -= 1_000_000_000;
|
||||
}
|
||||
|
||||
let duration = Duration::new(sec.abs() as u64, nanosec.abs() as u32);
|
||||
let duration = Duration::new(sec.unsigned_abs(), nanosec.unsigned_abs() as u32);
|
||||
Some(UNIX_EPOCH - duration)
|
||||
}
|
||||
else {
|
||||
|
@ -359,6 +384,11 @@ impl<'dir> File<'dir> {
|
|||
}
|
||||
}
|
||||
|
||||
#[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()
|
||||
|
@ -374,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
|
||||
|
@ -401,7 +432,21 @@ 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;
|
||||
|
@ -425,6 +470,22 @@ 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.
|
||||
|
@ -482,6 +543,7 @@ impl<'dir> FileTarget<'dir> {
|
|||
|
||||
/// More readable aliases for the permission bits exposed by libc.
|
||||
#[allow(trivial_numeric_casts)]
|
||||
#[cfg(unix)]
|
||||
mod modes {
|
||||
|
||||
// The `libc::mode_t` type’s actual type varies, but the value returned
|
||||
|
@ -559,6 +621,7 @@ mod filename_test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn topmost() {
|
||||
assert_eq!("/", File::filename(Path::new("/")))
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
use std::cmp::Ordering;
|
||||
use std::iter::FromIterator;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::fs::DotFilter;
|
||||
use crate::fs::File;
|
||||
|
@ -23,7 +23,7 @@ use crate::fs::File;
|
|||
/// 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
|
||||
|
@ -50,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
|
||||
|
@ -82,9 +59,6 @@ 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,
|
||||
}
|
||||
|
||||
|
@ -115,7 +89,7 @@ impl FileFilter {
|
|||
}
|
||||
|
||||
/// Sort the files in the given vector based on the sort field option.
|
||||
pub fn sort_files<'a, F>(&self, files: &mut Vec<F>)
|
||||
pub fn sort_files<'a, F>(&self, files: &mut [F])
|
||||
where F: AsRef<File<'a>>
|
||||
{
|
||||
files.sort_by(|a, b| {
|
||||
|
@ -139,7 +113,7 @@ impl FileFilter {
|
|||
|
||||
|
||||
/// 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
|
||||
|
@ -157,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”).
|
||||
|
@ -173,7 +148,7 @@ 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”).
|
||||
|
@ -182,7 +157,7 @@ pub enum SortField {
|
|||
/// 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”).
|
||||
|
@ -219,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
|
||||
|
@ -250,6 +225,7 @@ impl SortField {
|
|||
Self::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name),
|
||||
|
||||
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()),
|
||||
|
@ -284,8 +260,10 @@ impl SortField {
|
|||
}
|
||||
|
||||
fn strip_dot(n: &str) -> &str {
|
||||
if n.starts_with('.') { &n[1..] }
|
||||
else { n }
|
||||
match n.strip_prefix('.') {
|
||||
Some(s) => s,
|
||||
None => n,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,7 +271,7 @@ 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>,
|
||||
}
|
||||
|
@ -345,35 +323,20 @@ impl IgnorePatterns {
|
|||
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)]
|
||||
|
@ -383,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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::output::icons::FileIcon;
|
|||
use crate::theme::FileColours;
|
||||
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct FileExtensions;
|
||||
|
||||
impl FileExtensions {
|
||||
|
@ -19,13 +19,14 @@ 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.
|
||||
#[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", "pom.xml", "Rakefile", "package.json", "Gruntfile.js",
|
||||
"Gruntfile.coffee", "BUILD", "BUILD.bazel", "WORKSPACE", "build.xml",
|
||||
"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",
|
||||
|
@ -34,10 +35,11 @@ impl FileExtensions {
|
|||
|
||||
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", "heif",
|
||||
"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",
|
||||
])
|
||||
}
|
||||
|
||||
|
@ -79,14 +81,14 @@ impl FileExtensions {
|
|||
file.extension_is_one_of( &[
|
||||
"zip", "tar", "Z", "z", "gz", "bz2", "a", "ar", "7z",
|
||||
"iso", "dmg", "tc", "rar", "par", "tgz", "xz", "txz",
|
||||
"lz", "tlz", "lzma", "deb", "rpm", "zst",
|
||||
"lz", "tlz", "lzma", "deb", "rpm", "zst", "lz4", "cpio",
|
||||
])
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
28
src/main.rs
28
src/main.rs
|
@ -7,18 +7,19 @@
|
|||
#![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::find_map)]
|
||||
#![allow(clippy::map_unwrap_or)]
|
||||
#![allow(clippy::match_same_arms)]
|
||||
#![allow(clippy::missing_const_for_fn)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
#![allow(clippy::must_use_candidate)]
|
||||
#![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;
|
||||
|
@ -48,10 +49,20 @@ 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(|e| e.as_ref()), &LiveVars) {
|
||||
match Options::parse(args.iter().map(std::convert::AsRef::as_ref), &LiveVars) {
|
||||
OptionsResult::Ok(options, mut input_paths) => {
|
||||
|
||||
// List the current directory by default.
|
||||
|
@ -155,6 +166,9 @@ fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
|
@ -282,7 +296,7 @@ impl<'args> Exa<'args> {
|
|||
|
||||
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, filter, recurse, git_ignoring, git };
|
||||
let r = details::Render { dir, files, theme, file_style, opts, recurse, filter, git_ignoring, git };
|
||||
r.render(&mut self.writer)
|
||||
}
|
||||
|
||||
|
@ -306,7 +320,7 @@ impl<'args> Exa<'args> {
|
|||
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, filter, recurse, git_ignoring, git };
|
||||
let r = details::Render { dir, files, theme, file_style, opts, recurse, filter, git_ignoring, git };
|
||||
r.render(&mut self.writer)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::options::parser::{Arg, Flag, ParseError};
|
|||
|
||||
|
||||
/// Something wrong with the combination of options the user has picked.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum OptionsError {
|
||||
|
||||
/// There was an error (from `getopts`) parsing the arguments.
|
||||
|
@ -44,7 +44,7 @@ pub enum OptionsError {
|
|||
}
|
||||
|
||||
/// The source of a string that failed to be parsed as a number.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum NumberSource {
|
||||
|
||||
/// It came... from a command-line argument!
|
||||
|
@ -119,7 +119,7 @@ impl OptionsError {
|
|||
|
||||
|
||||
/// A list of legal choices for an argument-taking option.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Choices(pub &'static [&'static str]);
|
||||
|
||||
impl fmt::Display for Choices {
|
||||
|
|
|
@ -88,6 +88,7 @@ impl SortField {
|
|||
"cr" | "created" => {
|
||||
Self::CreatedDate
|
||||
}
|
||||
#[cfg(unix)]
|
||||
"inode" => {
|
||||
Self::FileInode
|
||||
}
|
||||
|
@ -294,7 +295,6 @@ mod test {
|
|||
mod ignore_patterns {
|
||||
use super::*;
|
||||
use std::iter::FromIterator;
|
||||
use glob;
|
||||
|
||||
fn pat(string: &'static str) -> glob::Pattern {
|
||||
glob::Pattern::new(string).unwrap()
|
||||
|
|
|
@ -69,7 +69,7 @@ static EXTENDED_HELP: &str = " -@, --extended list each file's extended
|
|||
/// 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, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct HelpString;
|
||||
|
||||
impl HelpString {
|
||||
|
|
|
@ -178,7 +178,7 @@ impl Options {
|
|||
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(format!(
|
||||
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"
|
||||
)));
|
||||
}
|
||||
|
@ -216,7 +216,7 @@ pub mod test {
|
|||
use crate::options::parser::{Arg, MatchedFlags};
|
||||
use std::ffi::OsStr;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum Strictnesses {
|
||||
Last,
|
||||
Complain,
|
||||
|
@ -228,14 +228,14 @@ 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
|
||||
{
|
||||
use self::Strictnesses::*;
|
||||
use crate::options::parser::{Args, Strictness};
|
||||
|
||||
let bits = inputs.into_iter().map(OsStr::new).collect::<Vec<_>>();
|
||||
let bits = inputs.iter().map(OsStr::new).collect::<Vec<_>>();
|
||||
let mut result = Vec::new();
|
||||
|
||||
if strictnesses == Last || strictnesses == Both {
|
||||
|
|
|
@ -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, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum Flag {
|
||||
Short(ShortArg),
|
||||
Long(LongArg),
|
||||
|
@ -77,7 +77,7 @@ impl fmt::Display for Flag {
|
|||
}
|
||||
|
||||
/// 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.
|
||||
|
@ -108,7 +108,7 @@ pub enum TakesValue {
|
|||
|
||||
|
||||
/// An **argument** can be matched by one of the user’s input strings.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct Arg {
|
||||
|
||||
/// The short argument that matches it, if any.
|
||||
|
@ -136,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 {
|
||||
|
@ -146,8 +146,6 @@ impl Args {
|
|||
pub fn parse<'args, I>(&self, inputs: I, strictness: Strictness) -> Result<Matches<'args>, ParseError>
|
||||
where I: IntoIterator<Item = &'args OsStr>
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
let mut parsing = true;
|
||||
|
||||
// The results that get built up.
|
||||
|
@ -159,7 +157,7 @@ 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
|
||||
|
@ -174,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
|
||||
|
@ -221,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
|
||||
|
@ -236,7 +234,7 @@ 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 {
|
||||
|
@ -291,7 +289,7 @@ impl Args {
|
|||
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() {
|
||||
|
@ -342,7 +340,7 @@ 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.
|
||||
|
@ -353,7 +351,7 @@ pub struct Matches<'args> {
|
|||
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
|
||||
|
@ -432,7 +430,7 @@ impl<'a> MatchedFlags<'a> {
|
|||
.filter(|tuple| tuple.1.is_some() && predicate(&tuple.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if those.len() < 2 { Ok(those.first().cloned().map(|t| t.1.unwrap())) }
|
||||
if those.len() < 2 { Ok(those.first().copied().map(|t| t.1.unwrap())) }
|
||||
else { Err(OptionsError::Duplicate(those[0].0, those[1].0)) }
|
||||
}
|
||||
else {
|
||||
|
@ -464,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.
|
||||
|
@ -495,19 +493,42 @@ impl fmt::Display for ParseError {
|
|||
}
|
||||
}
|
||||
|
||||
#[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..])))
|
||||
return Some((bytes_to_os_str(before),
|
||||
bytes_to_os_str(&after[1..])))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -722,6 +743,6 @@ mod matches_test {
|
|||
fn no_count() {
|
||||
let flags = MatchedFlags { flags: Vec::new(), strictness: Strictness::UseLastArguments };
|
||||
|
||||
assert_eq!(flags.has(&COUNT).unwrap(), false);
|
||||
assert!(!flags.has(&COUNT).unwrap());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ 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)?;
|
||||
let use_colours = UseColours::deduce(matches, vars)?;
|
||||
let colour_scale = ColourScale::deduce(matches)?;
|
||||
|
||||
let definitions = if use_colours == UseColours::Never {
|
||||
|
@ -21,10 +21,15 @@ impl Options {
|
|||
|
||||
|
||||
impl UseColours {
|
||||
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
|
||||
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(Self::Automatic),
|
||||
None => return Ok(default_value),
|
||||
};
|
||||
|
||||
if word == "always" {
|
||||
|
@ -87,6 +92,16 @@ mod terminal_test {
|
|||
}
|
||||
};
|
||||
|
||||
($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() {
|
||||
|
@ -95,11 +110,39 @@ mod terminal_test {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
($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.
|
||||
|
@ -111,6 +154,9 @@ mod terminal_test {
|
|||
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
|
||||
}
|
||||
|
@ -120,32 +166,33 @@ mod terminal_test {
|
|||
|
||||
|
||||
// Default
|
||||
test!(empty: UseColours <- []; Both => Ok(UseColours::Automatic));
|
||||
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"]; Both => Ok(UseColours::Always));
|
||||
test!(u_auto: UseColours <- ["--colour", "auto"]; Both => Ok(UseColours::Automatic));
|
||||
test!(u_never: UseColours <- ["--colour=never"]; Both => Ok(UseColours::Never));
|
||||
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"]; Both => Ok(UseColours::Always));
|
||||
test!(no_u_auto: UseColours <- ["--color=auto"]; Both => Ok(UseColours::Automatic));
|
||||
test!(no_u_never: UseColours <- ["--color", "never"]; Both => Ok(UseColours::Never));
|
||||
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"]; Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("upstream"))); // the error is for --color
|
||||
test!(u_error: UseColours <- ["--colour=lovers"]; Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one!
|
||||
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"]; Last => Ok(UseColours::Never));
|
||||
test!(overridden_2: UseColours <- ["--color=auto", "--colour=never"]; Last => Ok(UseColours::Never));
|
||||
test!(overridden_3: UseColours <- ["--colour=auto", "--color=never"]; Last => Ok(UseColours::Never));
|
||||
test!(overridden_4: UseColours <- ["--color=auto", "--color=never"]; Last => Ok(UseColours::Never));
|
||||
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"]; Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("colour")));
|
||||
test!(overridden_6: UseColours <- ["--color=auto", "--colour=never"]; Complain => err OptionsError::Duplicate(Flag::Long("color"), Flag::Long("colour")));
|
||||
test!(overridden_7: UseColours <- ["--colour=auto", "--color=never"]; Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("color")));
|
||||
test!(overridden_8: UseColours <- ["--color=auto", "--color=never"]; Complain => err OptionsError::Duplicate(Flag::Long("color"), Flag::Long("color")));
|
||||
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));
|
||||
|
|
|
@ -15,6 +15,9 @@ 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
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::options::flags;
|
|||
use crate::options::parser::MatchedFlags;
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct VersionString;
|
||||
// There were options here once, but there aren’t anymore!
|
||||
|
||||
|
|
|
@ -32,13 +32,10 @@ impl Mode {
|
|||
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 flag = match flag {
|
||||
Some(f) => f,
|
||||
None => {
|
||||
Self::strict_check_long_flags(matches)?;
|
||||
let grid = grid::Options::deduce(matches)?;
|
||||
return Ok(Self::Grid(grid));
|
||||
}
|
||||
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));
|
||||
};
|
||||
|
||||
if flag.matches(&flags::LONG)
|
||||
|
@ -57,10 +54,9 @@ impl Mode {
|
|||
let grid_details = grid_details::Options { grid, details, row_threshold };
|
||||
return Ok(Self::GridDetails(grid_details));
|
||||
}
|
||||
else {
|
||||
// the --tree case is handled by the DirAction parser later
|
||||
return Ok(Self::Details(details));
|
||||
}
|
||||
|
||||
// the --tree case is handled by the DirAction parser later
|
||||
return Ok(Self::Details(details));
|
||||
}
|
||||
|
||||
Self::strict_check_long_flags(matches)?;
|
||||
|
@ -195,7 +191,7 @@ impl TableOptions {
|
|||
let size_format = SizeFormat::deduce(matches)?;
|
||||
let user_format = UserFormat::deduce(matches)?;
|
||||
let columns = Columns::deduce(matches)?;
|
||||
Ok(Self { time_format, size_format, columns , user_format})
|
||||
Ok(Self { size_format, time_format, user_format, columns })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -215,7 +211,7 @@ impl Columns {
|
|||
let filesize = ! matches.has(&flags::NO_FILESIZE)?;
|
||||
let user = ! matches.has(&flags::NO_USER)?;
|
||||
|
||||
Ok(Self { time_types, git, octal, blocks, group, inode, links, permissions, filesize, user })
|
||||
Ok(Self { time_types, inode, links, blocks, group, git, octal, permissions, filesize, user })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -382,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)) {
|
||||
|
@ -393,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)) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -193,7 +191,7 @@ impl TextCellContents {
|
|||
///
|
||||
/// 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 {
|
||||
|
|
|
@ -91,7 +91,7 @@ use crate::theme::Theme;
|
|||
///
|
||||
/// Almost all the heavy lifting is done in a Table object, which handles the
|
||||
/// columns for each row.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Options {
|
||||
|
||||
/// Options specific to drawing a table.
|
||||
|
@ -147,7 +147,11 @@ impl<'a> AsRef<File<'a>> for Egg<'a> {
|
|||
|
||||
impl<'a> Render<'a> {
|
||||
pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
|
||||
let mut pool = Pool::new(num_cpus::get() as u32);
|
||||
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 {
|
||||
|
@ -157,7 +161,7 @@ impl<'a> Render<'a> {
|
|||
(None, _) => {/* Keep Git how it is */},
|
||||
}
|
||||
|
||||
let mut table = Table::new(table, self.git, &self.theme);
|
||||
let mut table = Table::new(table, self.git, self.theme);
|
||||
|
||||
if self.opts.header {
|
||||
let header = table.header_row();
|
||||
|
@ -350,9 +354,10 @@ impl<'a> Render<'a> {
|
|||
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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use ansi_term::{ANSIString, Style};
|
||||
|
||||
|
||||
pub fn escape<'a>(string: String, bits: &mut Vec<ANSIString<'a>>, good: Style, bad: Style) {
|
||||
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;
|
||||
|
|
|
@ -54,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.
|
||||
|
@ -73,7 +73,7 @@ impl Default for Classify {
|
|||
|
||||
|
||||
/// Whether and how to show icons.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum ShowIcons {
|
||||
|
||||
/// Don’t show icons at all.
|
||||
|
@ -226,7 +226,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
|||
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(
|
||||
|
@ -235,12 +235,13 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
|||
self.colours.symlink_path(),
|
||||
self.colours.control_char(),
|
||||
);
|
||||
bits.push(self.colours.symlink_path().paint("/"));
|
||||
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.
|
||||
#[cfg(unix)]
|
||||
fn classify_char(&self, file: &File<'_>) -> Option<&'static str> {
|
||||
if file.is_executable_file() {
|
||||
Some("*")
|
||||
|
@ -262,6 +263,19 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
|||
}
|
||||
}
|
||||
|
||||
#[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.
|
||||
///
|
||||
|
@ -301,11 +315,16 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
|
|||
|
||||
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(),
|
||||
_ => self.colours.colour_file(self.file),
|
||||
|
|
|
@ -8,7 +8,7 @@ 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,
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ impl<'a> Render<'a> {
|
|||
grid.add(tg::Cell {
|
||||
contents: filename.strings().to_string(),
|
||||
width: *filename.width(),
|
||||
alignment: tg::Alignment::Left,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ use crate::output::tree::{TreeParams, TreeDepth};
|
|||
use crate::theme::Theme;
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Options {
|
||||
pub grid: GridOptions,
|
||||
pub details: DetailsOptions,
|
||||
|
@ -39,7 +39,7 @@ impl 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(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum RowThreshold {
|
||||
|
||||
/// Only use grid-details view if it would result in at least this many
|
||||
|
@ -158,7 +158,7 @@ impl<'a> Render<'a> {
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
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));
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ impl<'a> Render<'a> {
|
|||
if the_grid_fits {
|
||||
last_working_grid = grid;
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
|
@ -202,7 +202,7 @@ impl<'a> Render<'a> {
|
|||
(None, _) => {/* Keep Git how it is */},
|
||||
}
|
||||
|
||||
let mut table = Table::new(options, self.git, &self.theme);
|
||||
let mut table = Table::new(options, self.git, self.theme);
|
||||
let mut rows = Vec::new();
|
||||
|
||||
if self.details.header {
|
||||
|
@ -263,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);
|
||||
|
@ -276,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);
|
||||
|
|
|
@ -37,7 +37,7 @@ impl Icons {
|
|||
/// - 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<'a>(style: Style) -> Style {
|
||||
pub fn iconify_style(style: Style) -> Style {
|
||||
style.background.or(style.foreground)
|
||||
.map(Style::from)
|
||||
.unwrap_or_default()
|
||||
|
@ -69,7 +69,9 @@ lazy_static! {
|
|||
m.insert("Dockerfile", '\u{f308}'); //
|
||||
m.insert("ds_store", '\u{f179}'); //
|
||||
m.insert("gitignore_global", '\u{f1d3}'); //
|
||||
m.insert("gradle", '\u{e70e}'); //
|
||||
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}'); //
|
||||
|
@ -80,9 +82,10 @@ lazy_static! {
|
|||
m.insert("include", '\u{e5fc}'); //
|
||||
m.insert("lib", '\u{f121}'); //
|
||||
m.insert("localized", '\u{f179}'); //
|
||||
m.insert("Makefile", '\u{e779}'); //
|
||||
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}'); //
|
||||
|
||||
|
@ -110,6 +113,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"apk" => '\u{e70e}', //
|
||||
"apple" => '\u{f179}', //
|
||||
"avi" => '\u{f03d}', //
|
||||
"avif" => '\u{f1c5}', //
|
||||
"avro" => '\u{e60b}', //
|
||||
"awk" => '\u{f489}', //
|
||||
"bash" => '\u{f489}', //
|
||||
|
@ -117,6 +121,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"bash_profile" => '\u{f489}', //
|
||||
"bashrc" => '\u{f489}', //
|
||||
"bat" => '\u{f17a}', //
|
||||
"bats" => '\u{f489}', //
|
||||
"bmp" => '\u{f1c5}', //
|
||||
"bz" => '\u{f410}', //
|
||||
"bz2" => '\u{f410}', //
|
||||
|
@ -133,14 +138,15 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"coffee" => '\u{f0f4}', //
|
||||
"conf" => '\u{e615}', //
|
||||
"cp" => '\u{e61d}', //
|
||||
"cpio" => '\u{f410}', //
|
||||
"cpp" => '\u{e61d}', //
|
||||
"cs" => '\u{f81a}', //
|
||||
"cs" => '\u{f031b}', //
|
||||
"csh" => '\u{f489}', //
|
||||
"cshtml" => '\u{f1fa}', //
|
||||
"csproj" => '\u{f81a}', //
|
||||
"csproj" => '\u{f031b}', //
|
||||
"css" => '\u{e749}', //
|
||||
"csv" => '\u{f1c3}', //
|
||||
"csx" => '\u{f81a}', //
|
||||
"csx" => '\u{f031b}', //
|
||||
"cxx" => '\u{e61d}', //
|
||||
"d" => '\u{e7af}', //
|
||||
"dart" => '\u{e798}', //
|
||||
|
@ -155,6 +161,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"DS_store" => '\u{f179}', //
|
||||
"dump" => '\u{f1c0}', //
|
||||
"ebook" => '\u{e28b}', //
|
||||
"ebuild" => '\u{f30d}', //
|
||||
"editorconfig" => '\u{e615}', //
|
||||
"ejs" => '\u{e618}', //
|
||||
"elm" => '\u{e62c}', //
|
||||
|
@ -170,6 +177,9 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"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}', //
|
||||
|
@ -181,7 +191,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"gitignore" => '\u{f1d3}', //
|
||||
"gitmodules" => '\u{f1d3}', //
|
||||
"go" => '\u{e626}', //
|
||||
"gradle" => '\u{e70e}', //
|
||||
"gradle" => '\u{e256}', //
|
||||
"groovy" => '\u{e775}', //
|
||||
"gsheet" => '\u{f1c3}', //
|
||||
"gslides" => '\u{f1c4}', //
|
||||
|
@ -196,28 +206,41 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"hxx" => '\u{f0fd}', //
|
||||
"ico" => '\u{f1c5}', //
|
||||
"image" => '\u{f1c5}', //
|
||||
"img" => '\u{e271}', //
|
||||
"iml" => '\u{e7b5}', //
|
||||
"ini" => '\u{f17a}', //
|
||||
"ipynb" => '\u{e606}', //
|
||||
"ipynb" => '\u{e678}', //
|
||||
"iso" => '\u{e271}', //
|
||||
"j2c" => '\u{f1c5}', //
|
||||
"j2k" => '\u{f1c5}', //
|
||||
"jad" => '\u{e256}', //
|
||||
"jar" => '\u{e204}', //
|
||||
"java" => '\u{e204}', //
|
||||
"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{f718}', //
|
||||
"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}', //
|
||||
|
@ -227,6 +250,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"markdown" => '\u{f48a}', //
|
||||
"md" => '\u{f48a}', //
|
||||
"mjs" => '\u{e74e}', //
|
||||
"mk" => '\u{f489}', //
|
||||
"mkd" => '\u{f48a}', //
|
||||
"mkv" => '\u{f03d}', //
|
||||
"mobi" => '\u{e28b}', //
|
||||
|
@ -236,7 +260,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"msi" => '\u{e70f}', //
|
||||
"mustache" => '\u{e60f}', //
|
||||
"nix" => '\u{f313}', //
|
||||
"node" => '\u{f898}', //
|
||||
"node" => '\u{f0399}', //
|
||||
"npmignore" => '\u{e71e}', //
|
||||
"odp" => '\u{f1c4}', //
|
||||
"ods" => '\u{f1c3}', //
|
||||
|
@ -244,11 +268,15 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"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}', //
|
||||
|
@ -276,7 +304,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"rspec_parallel"=> '\u{e21e}', //
|
||||
"rspec_status" => '\u{e21e}', //
|
||||
"rss" => '\u{f09e}', //
|
||||
"rtf" => '\u{f718}', //
|
||||
"rtf" => '\u{f0219}', //
|
||||
"ru" => '\u{e21e}', //
|
||||
"rubydoc" => '\u{e73b}', //
|
||||
"sass" => '\u{e603}', //
|
||||
|
@ -289,28 +317,34 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"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{fd42}', // ﵂
|
||||
"vue" => '\u{f0844}', //
|
||||
"war" => '\u{e256}', //
|
||||
"wav" => '\u{f001}', //
|
||||
"webm" => '\u{f03d}', //
|
||||
|
@ -321,8 +355,8 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"xhtml" => '\u{f13b}', //
|
||||
"xls" => '\u{f1c3}', //
|
||||
"xlsx" => '\u{f1c3}', //
|
||||
"xml" => '\u{fabf}', // 謹
|
||||
"xul" => '\u{fabf}', // 謹
|
||||
"xml" => '\u{f05c0}', //
|
||||
"xul" => '\u{f05c0}', //
|
||||
"xz" => '\u{f410}', //
|
||||
"yaml" => '\u{f481}', //
|
||||
"yml" => '\u{f481}', //
|
||||
|
@ -330,6 +364,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
|
|||
"zsh" => '\u{f489}', //
|
||||
"zsh-theme" => '\u{f489}', //
|
||||
"zshrc" => '\u{f489}', //
|
||||
"zst" => '\u{f410}', //
|
||||
_ => '\u{f15b}' //
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ pub struct View {
|
|||
|
||||
|
||||
/// The **mode** is the “type” of output.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Mode {
|
||||
Grid(grid::Options),
|
||||
|
@ -37,7 +37,7 @@ pub enum Mode {
|
|||
|
||||
|
||||
/// The width of the terminal requested by the user.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum TerminalWidth {
|
||||
|
||||
/// The user requested this specific number of columns.
|
||||
|
@ -55,7 +55,7 @@ impl TerminalWidth {
|
|||
|
||||
match self {
|
||||
Self::Set(width) => Some(width),
|
||||
Self::Automatic => term_size::dimensions_stdout().map(|t| t.0),
|
||||
Self::Automatic => terminal_size::terminal_size().map(|(w, _)| w.0.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ 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;
|
||||
|
@ -84,7 +85,7 @@ pub mod test {
|
|||
].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&TestColours).into())
|
||||
assert_eq!(expected, stati.render(&TestColours))
|
||||
}
|
||||
|
||||
|
||||
|
@ -103,6 +104,6 @@ pub mod test {
|
|||
].into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, stati.render(&TestColours).into())
|
||||
assert_eq!(expected, stati.render(&TestColours))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ pub mod test {
|
|||
|
||||
#[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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,7 +28,9 @@ 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;
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::output::cell::TextCell;
|
|||
|
||||
impl f::OctalPermissions {
|
||||
fn bits_to_octal(r: bool, w: bool, x: bool) -> u8 {
|
||||
(r as u8) * 4 + (w as u8) * 2 + (x as u8)
|
||||
u8::from(r) * 4 + u8::from(w) * 2 + u8::from(x)
|
||||
}
|
||||
|
||||
pub fn render(&self, style: Style) -> TextCell {
|
||||
|
@ -40,7 +40,7 @@ pub mod test {
|
|||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "0755");
|
||||
assert_eq!(expected, octal.render(Purple.bold()).into());
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -54,7 +54,7 @@ pub mod test {
|
|||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "0644");
|
||||
assert_eq!(expected, octal.render(Purple.bold()).into());
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -68,7 +68,7 @@ pub mod test {
|
|||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "0600");
|
||||
assert_eq!(expected, octal.render(Purple.bold()).into());
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -82,7 +82,7 @@ pub mod test {
|
|||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "4777");
|
||||
assert_eq!(expected, octal.render(Purple.bold()).into());
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ pub mod test {
|
|||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "2777");
|
||||
assert_eq!(expected, octal.render(Purple.bold()).into());
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -111,6 +111,6 @@ pub mod test {
|
|||
let octal = f::OctalPermissions{ permissions: bits };
|
||||
|
||||
let expected = TextCell::paint_str(Purple.bold(), "1777");
|
||||
assert_eq!(expected, octal.render(Purple.bold()).into());
|
||||
assert_eq!(expected, octal.render(Purple.bold()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ 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,6 +23,17 @@ 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -46,7 +46,7 @@ impl f::Size {
|
|||
} else {
|
||||
numerics.format_int(n.round() as isize)
|
||||
};
|
||||
|
||||
|
||||
TextCell {
|
||||
// symbol is guaranteed to be ASCII since unit prefixes are hardcoded.
|
||||
width: DisplayWidth::from(&*number) + symbol.len(),
|
||||
|
@ -153,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,6 +1,7 @@
|
|||
use std::cmp::max;
|
||||
use std::env;
|
||||
use std::ops::Deref;
|
||||
#[cfg(unix)]
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use datetime::TimeZone;
|
||||
|
@ -8,6 +9,7 @@ use zoneinfo_compiled::{CompiledData, Result as TZResult};
|
|||
|
||||
use lazy_static::lazy_static;
|
||||
use log::*;
|
||||
#[cfg(unix)]
|
||||
use users::UsersCache;
|
||||
|
||||
use crate::fs::{File, fields as f};
|
||||
|
@ -19,7 +21,7 @@ use crate::theme::Theme;
|
|||
|
||||
|
||||
/// Options for displaying a table.
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Options {
|
||||
pub size_format: SizeFormat,
|
||||
pub time_format: TimeFormat,
|
||||
|
@ -28,7 +30,8 @@ pub struct Options {
|
|||
}
|
||||
|
||||
/// Extra columns to display in the table.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct Columns {
|
||||
|
||||
/// At least one of these timestamps will be shown.
|
||||
|
@ -53,10 +56,12 @@ impl Columns {
|
|||
let mut columns = Vec::with_capacity(4);
|
||||
|
||||
if self.inode {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Inode);
|
||||
}
|
||||
|
||||
if self.octal {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Octal);
|
||||
}
|
||||
|
||||
|
@ -65,6 +70,7 @@ impl Columns {
|
|||
}
|
||||
|
||||
if self.links {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::HardLinks);
|
||||
}
|
||||
|
||||
|
@ -73,14 +79,17 @@ impl Columns {
|
|||
}
|
||||
|
||||
if self.blocks {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Blocks);
|
||||
}
|
||||
|
||||
if self.user {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::User);
|
||||
}
|
||||
|
||||
if self.group {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Group);
|
||||
}
|
||||
|
||||
|
@ -115,12 +124,18 @@ 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,
|
||||
}
|
||||
|
||||
|
@ -135,6 +150,7 @@ pub enum Alignment {
|
|||
impl Column {
|
||||
|
||||
/// Get the alignment this column should use.
|
||||
#[cfg(unix)]
|
||||
pub fn alignment(self) -> Alignment {
|
||||
match self {
|
||||
Self::FileSize |
|
||||
|
@ -146,19 +162,37 @@ impl Column {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn alignment(&self) -> Alignment {
|
||||
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 {
|
||||
#[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",
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +200,8 @@ impl Column {
|
|||
|
||||
|
||||
/// 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”,
|
||||
|
@ -182,7 +217,7 @@ pub enum SizeFormat {
|
|||
}
|
||||
|
||||
/// Formatting options for user and group.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum UserFormat {
|
||||
/// The UID / GID
|
||||
Numeric,
|
||||
|
@ -199,7 +234,7 @@ impl Default for SizeFormat {
|
|||
|
||||
/// 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`).
|
||||
|
@ -234,7 +269,7 @@ impl TimeType {
|
|||
///
|
||||
/// 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,
|
||||
|
@ -272,10 +307,12 @@ pub struct Environment {
|
|||
tz: Option<TimeZone>,
|
||||
|
||||
/// Mapping cache of user IDs to usernames.
|
||||
#[cfg(unix)]
|
||||
users: Mutex<UsersCache>,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
#[cfg(unix)]
|
||||
pub fn lock_users(&self) -> MutexGuard<'_, UsersCache> {
|
||||
self.users.lock().unwrap()
|
||||
}
|
||||
|
@ -294,21 +331,23 @@ impl Environment {
|
|||
let numeric = locale::Numeric::load_user_locale()
|
||||
.unwrap_or_else(|_| locale::Numeric::english());
|
||||
|
||||
#[cfg(unix)]
|
||||
let users = Mutex::new(UsersCache::new());
|
||||
|
||||
Self { tz, numeric, users }
|
||||
Self { numeric, tz, #[cfg(unix)] users }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn determine_time_zone() -> TZResult<TimeZone> {
|
||||
if let Ok(file) = env::var("TZ") {
|
||||
TimeZone::from_file({
|
||||
if file.starts_with("/") {
|
||||
if file.starts_with('/') {
|
||||
file
|
||||
} else {
|
||||
format!("/usr/share/zoneinfo/{}", {
|
||||
if file.starts_with(":") {
|
||||
file.replacen(":", "", 1)
|
||||
if file.starts_with(':') {
|
||||
file.replacen(':', "", 1)
|
||||
} else {
|
||||
file
|
||||
}
|
||||
|
@ -320,6 +359,31 @@ fn determine_time_zone() -> TZResult<TimeZone> {
|
|||
}
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
|
@ -386,11 +450,15 @@ impl<'a, 'f> Table<'a> {
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn octal_permissions(&self, file: &File<'_>) -> f::OctalPermissions {
|
||||
f::OctalPermissions {
|
||||
permissions: file.permissions(),
|
||||
|
@ -405,24 +473,30 @@ impl<'a, 'f> Table<'a> {
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ use unicode_width::UnicodeWidthStr;
|
|||
///
|
||||
/// Currently exa does not support *custom* styles, where the user enters a
|
||||
/// format string in an environment variable or something. Just these four.
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum TimeFormat {
|
||||
|
||||
/// The **default format** uses the user’s locale to print month names,
|
||||
|
@ -87,7 +87,7 @@ fn default_zoned(time: SystemTime, zone: &TimeZone) -> String {
|
|||
}
|
||||
|
||||
fn get_dateformat(date: &LocalDateTime) -> &'static DateFormat<'static> {
|
||||
match (is_recent(&date), *MAXIMUM_MONTH_WIDTH) {
|
||||
match (is_recent(date), *MAXIMUM_MONTH_WIDTH) {
|
||||
(true, 4) => &FOUR_WIDE_DATE_TIME,
|
||||
(true, 5) => &FIVE_WIDE_DATE_TIME,
|
||||
(true, _) => &OTHER_WIDE_DATE_TIME,
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
//! each directory)
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum TreePart {
|
||||
|
||||
/// Rightmost column, *not* the last in the directory.
|
||||
|
@ -253,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());
|
||||
}
|
||||
|
@ -273,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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ pub use self::lsc::LSColors;
|
|||
mod default_theme;
|
||||
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Options {
|
||||
|
||||
pub use_colours: UseColours,
|
||||
|
@ -31,7 +31,7 @@ pub struct Options {
|
|||
/// 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, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum UseColours {
|
||||
|
||||
/// Display them even when output isn’t going to a terminal.
|
||||
|
@ -44,13 +44,13 @@ pub enum UseColours {
|
|||
Never,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum ColourScale {
|
||||
Fixed,
|
||||
Gradient,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Default)]
|
||||
#[derive(PartialEq, Eq, Debug, Default)]
|
||||
pub struct Definitions {
|
||||
pub ls: Option<String>,
|
||||
pub exa: Option<String>,
|
||||
|
@ -229,6 +229,7 @@ impl render::GitColours for Theme {
|
|||
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 }
|
||||
|
@ -261,11 +262,11 @@ impl render::SizeColours for Theme {
|
|||
use number_prefix::Prefix::*;
|
||||
|
||||
match prefix {
|
||||
None => self.ui.size.number_byte,
|
||||
Some(Kilo) | Some(Kibi) => self.ui.size.number_kilo,
|
||||
Some(Mega) | Some(Mebi) => self.ui.size.number_mega,
|
||||
Some(Giga) | Some(Gibi) => self.ui.size.number_giga,
|
||||
Some(_) => self.ui.size.number_huge,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,11 +274,11 @@ impl render::SizeColours for Theme {
|
|||
use number_prefix::Prefix::*;
|
||||
|
||||
match prefix {
|
||||
None => self.ui.size.unit_byte,
|
||||
Some(Kilo) | Some(Kibi) => self.ui.size.unit_kilo,
|
||||
Some(Mega) | Some(Mebi) => self.ui.size.unit_mega,
|
||||
Some(Giga) | Some(Gibi) => self.ui.size.unit_giga,
|
||||
Some(_) => self.ui.size.unit_huge,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -287,6 +288,7 @@ impl render::SizeColours for Theme {
|
|||
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 }
|
||||
|
|
|
@ -144,6 +144,18 @@ status = 0
|
|||
tags = [ 'long', 'git' ]
|
||||
|
||||
|
||||
|
||||
# The forth Git repo: non UTF-8 file
|
||||
|
||||
[[cmd]]
|
||||
name = "‘exa --git -l’ handles non UTF8 file in Git repositories"
|
||||
shell = "exa --git -l /testcases/git4"
|
||||
stdout = { file = "outputs/git4_long.ansitxt" }
|
||||
stderr = { empty = true }
|
||||
status = 0
|
||||
tags = [ 'long', 'git' ]
|
||||
|
||||
|
||||
# Both repositories 1 and 2 at once
|
||||
|
||||
[[cmd]]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
.[1;33mr[31mw[0m[38;5;244m-[33mr[31mw[38;5;244m-[33mr[38;5;244m--[0m [1;32m0[0m cassowary [34m 1 Jan 12:34[0m [38;5;244m-[32mN[0m P[31m\u{8}[0m<30>UUU
|
Loading…
Reference in New Issue