diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 4071b3ad..05b7d542 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - rust: [1.48.0, stable, beta, nightly] + rust: [1.56.1, stable, beta, nightly] steps: - name: Checkout repository diff --git a/.gitignore b/.gitignore index ede6ac03..cb7baef0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ target # Vagrant stuff .vagrant -ubuntu-xenial-16.04-cloudimg-console.log +*.log # Compiled artifacts # (see devtools/*-package-for-*.sh) diff --git a/Cargo.lock b/Cargo.lock index f62bbc9a..5ee181df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,9 +279,9 @@ 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", ] diff --git a/Cargo.toml b/Cargo.toml index 9b384c39..45071b09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ natord = "1.0" num_cpus = "1.10" number_prefix = "0.4" scoped_threadpool = "0.1" -term_grid = "0.1" +term_grid = "0.2.0" terminal_size = "0.1.16" unicode-width = "0.1" zoneinfo_compiled = "0.5.1" diff --git a/README.md b/README.md index ec4749f0..71144fc6 100644 --- a/README.md +++ b/README.md @@ -105,74 +105,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. - $ 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 @@ -185,7 +184,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`. @@ -197,8 +196,8 @@ To build without Git support, run `cargo install --no-default-features exa` is a

Development - - Rust 1.45.2+ + + Rust 1.56.1+ @@ -207,16 +206,16 @@ To build without Git support, run `cargo install --no-default-features exa` is a

exa is written in [Rust](https://www.rust-lang.org/). -You will need rustc version 1.45.2 or higher. +You will need rustc version 1.56.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`. diff --git a/man/exa.1.md b/man/exa.1.md index bd91a7c0..7bafd3f7 100644 --- a/man/exa.1.md +++ b/man/exa.1.md @@ -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. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..071aaaa8 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.56.1" diff --git a/src/info/filetype.rs b/src/info/filetype.rs index 3011d5f0..d7d2a4d8 100644 --- a/src/info/filetype.rs +++ b/src/info/filetype.rs @@ -38,7 +38,8 @@ impl FileExtensions { "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", + "ico", "cr2", "orf", "nef", "heif", "avif", "jxl", "j2k", "jp2", + "j2c", "jpx", ]) } @@ -80,7 +81,7 @@ 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", "lz4", + "lz", "tlz", "lzma", "deb", "rpm", "zst", "lz4", "cpio", ]) } diff --git a/src/main.rs b/src/main.rs index 7bb193a2..26dcad1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,6 @@ #![allow(clippy::non_ascii_literal)] #![allow(clippy::option_if_let_else)] #![allow(clippy::too_many_lines)] -#![allow(clippy::unnested_or_patterns)] // TODO: remove this when we support Rust 1.53.0 #![allow(clippy::unused_self)] #![allow(clippy::upper_case_acronyms)] #![allow(clippy::wildcard_imports)] @@ -50,6 +49,10 @@ mod theme; fn main() { use std::process::exit; + unsafe { + libc::signal(libc::SIGPIPE, libc::SIG_DFL); + } + logger::configure(env::var_os(vars::EXA_DEBUG)); #[cfg(windows)] diff --git a/src/options/theme.rs b/src/options/theme.rs index 02309f71..010de4ad 100644 --- a/src/options/theme.rs +++ b/src/options/theme.rs @@ -5,7 +5,7 @@ use crate::theme::{Options, UseColours, ColourScale, Definitions}; impl Options { pub fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { - 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 { + fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { + 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 { + return MockVars { + ls: "", + exa: "", + no_color: "", + }; + } + fn with_no_color() -> MockVars { + return 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)); diff --git a/src/options/vars.rs b/src/options/vars.rs index a8fc40e1..9ce6cc57 100644 --- a/src/options/vars.rs +++ b/src/options/vars.rs @@ -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 diff --git a/src/output/details.rs b/src/output/details.rs index 9dca7d40..62ef7d82 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -147,7 +147,11 @@ impl<'a> AsRef> for Egg<'a> { impl<'a> Render<'a> { pub fn render(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 { diff --git a/src/output/grid.rs b/src/output/grid.rs index 290ee8b3..0e1b6942 100644 --- a/src/output/grid.rs +++ b/src/output/grid.rs @@ -46,6 +46,7 @@ impl<'a> Render<'a> { grid.add(tg::Cell { contents: filename.strings().to_string(), width: *filename.width(), + alignment: tg::Alignment::Left, }); } diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index 088d7a87..fd096da7 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -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); diff --git a/src/output/icons.rs b/src/output/icons.rs index eb7666a8..30ffdfc8 100644 --- a/src/output/icons.rs +++ b/src/output/icons.rs @@ -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}'); //  @@ -83,6 +85,7 @@ lazy_static! { 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}'); //  @@ -118,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}', //  @@ -134,6 +138,7 @@ 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}', //  "csh" => '\u{f489}', //  @@ -156,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}', //  @@ -185,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}', //  @@ -204,15 +210,21 @@ pub fn icon_for_file(file: &File<'_>) -> char { "ini" => '\u{f17a}', //  "ipynb" => '\u{e606}', //  "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}', //  @@ -255,6 +267,7 @@ 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}', //  @@ -314,6 +327,7 @@ pub fn icon_for_file(file: &File<'_>) -> char { "tiff" => '\u{f1c5}', //  "tlz" => '\u{f410}', //  "toml" => '\u{e615}', //  + "torrent" => '\u{e275}', //  "ts" => '\u{e628}', //  "tsv" => '\u{f1c3}', //  "tsx" => '\u{e7ba}', //  @@ -336,8 +350,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{f121}', //  + "xul" => '\u{f121}', //  "xz" => '\u{f410}', //  "yaml" => '\u{f481}', //  "yml" => '\u{f481}', //  @@ -345,6 +359,7 @@ pub fn icon_for_file(file: &File<'_>) -> char { "zsh" => '\u{f489}', //  "zsh-theme" => '\u{f489}', //  "zshrc" => '\u{f489}', //  + "zst" => '\u{f410}', //  _ => '\u{f15b}' //  } }