├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENCE ├── Makefile ├── README.md ├── Vagrantfile ├── build.rs ├── contrib ├── completions.bash ├── completions.fish ├── completions.zsh └── man │ └── exa.1 ├── devtools ├── README.md ├── dev-bash.sh ├── dev-download-and-check-release.sh ├── dev-generate-checksums.sh ├── dev-help-testvm.sh ├── dev-help.sh ├── dev-package-for-linux.sh ├── dev-run-debug.sh ├── dev-run-release.sh ├── dev-versions.sh └── local-package-for-macos.sh ├── screenshots.png ├── snap ├── .gitignore └── snapcraft.yaml ├── src ├── bin │ └── main.rs ├── exa.rs ├── fs │ ├── dir.rs │ ├── dir_action.rs │ ├── feature │ │ ├── git.rs │ │ ├── mod.rs │ │ └── xattr.rs │ ├── fields.rs │ ├── file.rs │ ├── filter.rs │ └── mod.rs ├── info │ ├── filetype.rs │ ├── mod.rs │ └── sources.rs ├── options │ ├── dir_action.rs │ ├── filter.rs │ ├── flags.rs │ ├── help.rs │ ├── misfire.rs │ ├── mod.rs │ ├── parser.rs │ ├── style.rs │ ├── vars.rs │ ├── version.rs │ └── view.rs ├── output │ ├── cell.rs │ ├── details.rs │ ├── escape.rs │ ├── file_name.rs │ ├── grid.rs │ ├── grid_details.rs │ ├── icons.rs │ ├── lines.rs │ ├── mod.rs │ ├── render │ │ ├── blocks.rs │ │ ├── filetype.rs │ │ ├── git.rs │ │ ├── groups.rs │ │ ├── inode.rs │ │ ├── links.rs │ │ ├── mod.rs │ │ ├── permissions.rs │ │ ├── size.rs │ │ ├── times.rs │ │ └── users.rs │ ├── table.rs │ ├── time.rs │ └── tree.rs └── style │ ├── colours.rs │ ├── lsc.rs │ └── mod.rs └── xtests ├── README.md ├── attributes ├── attributes_dir ├── attributes_files ├── dates_accessed ├── dates_deifidom ├── dates_fr ├── dates_full_iso ├── dates_iso ├── dates_jp ├── dates_long_iso ├── dates_modified ├── dir_paths ├── dirs ├── empty ├── error_duplicate ├── error_long ├── error_lt ├── error_ltr ├── error_overvalued ├── error_setting ├── error_short ├── error_twice ├── error_useless ├── error_value ├── file-names-exts ├── file-names-exts-bw ├── file-names-exts-case ├── file-names-exts-ext ├── file-names-exts-ext-case ├── file_names ├── file_names_1 ├── file_names_R ├── file_names_T ├── file_names_T@ ├── file_names_bw ├── file_names_x ├── files ├── files_120 ├── files_160 ├── files_200 ├── files_40 ├── files_80 ├── files_l ├── files_lG_120 ├── files_lG_160 ├── files_lG_200 ├── files_lG_40 ├── files_lG_80 ├── files_l_binary ├── files_l_bw ├── files_l_bytes ├── files_l_scale ├── files_lh ├── files_lhb ├── files_lhb2 ├── files_star_100 ├── files_star_150 ├── files_star_200 ├── files_star_lG_100 ├── files_star_lG_150 ├── files_star_lG_200 ├── git_12 ├── git_1212 ├── git_1_additions ├── git_1_both ├── git_1_edits ├── git_1_file ├── git_1_files ├── git_1_long ├── git_1_nogit ├── git_1_recurse ├── git_1_tree ├── git_21221 ├── git_2_all ├── git_2_ignore_recurse ├── git_2_ignore_tree ├── git_2_ignoreds ├── git_2_long ├── git_2_nogit ├── git_2_recurse ├── git_2_repository ├── git_2_target ├── git_2_tree ├── help ├── help_long ├── hiddens ├── hiddens_a ├── hiddens_aa ├── hiddens_l ├── hiddens_la ├── hiddens_laa ├── ignores_ogg ├── links ├── links_1 ├── links_1_files ├── links_T ├── links_T@ ├── links_bw ├── passwd ├── permissions ├── permissions_sudo ├── proc_1_root ├── proc_1_root_@ ├── run.sh ├── sort-by-type ├── specials ├── specials_F ├── specials_sort ├── themed_compresseds ├── themed_compresseds_r ├── themed_links ├── themed_long ├── themed_specials └── themed_un /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust stuff 2 | target 3 | 4 | # Vagrant stuff 5 | .vagrant 6 | ubuntu-xenial-16.04-cloudimg-console.log 7 | 8 | # Compiled artifacts 9 | # (see devtools/*-package-for-*.sh) 10 | /exa-linux-x86_64 11 | /exa-linux-x86_64-*.zip 12 | /exa-macos-x86_64 13 | /exa-macos-x86_64-*.zip 14 | /MD5SUMS 15 | /SHA1SUMS 16 | 17 | # Snap stuff 18 | parts 19 | prime 20 | stage 21 | *.snap 22 | .idea 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - sudo add-apt-repository --yes ppa:kubuntu-ppa/backports 3 | - sudo apt-get update -qq 4 | - sudo apt-get install cmake 5 | sudo: true 6 | language: rust 7 | rust: 8 | - stable 9 | script: 10 | - cargo build --verbose 11 | - cargo test --verbose 12 | - cargo build --verbose --no-default-features 13 | - cargo test --verbose --no-default-features 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "exa" 3 | version = "0.9.0" 4 | authors = [ "Benjamin Sago " ] 5 | build = "build.rs" 6 | edition = "2018" 7 | 8 | description = "A modern replacement for ls" 9 | homepage = "https://the.exa.website/" 10 | repository = "https://github.com/ogham/exa" 11 | documentation = "https://github.com/ogham/exa" 12 | 13 | readme = "README.md" 14 | categories = ["command-line-utilities"] 15 | keywords = ["ls", "files", "command-line"] 16 | license = "MIT" 17 | exclude = ["/devtools/*", "/Makefile", "/Vagrantfile", "/screenshots.png"] 18 | 19 | 20 | [[bin]] 21 | name = "exa" 22 | path = "src/bin/main.rs" 23 | doc = false 24 | 25 | 26 | [lib] 27 | name = "exa" 28 | path = "src/exa.rs" 29 | 30 | [dependencies] 31 | ansi_term = "0.12.0" 32 | datetime = "0.5" 33 | env_logger = "0.9" 34 | glob = "0.3.0" 35 | lazy_static = "1.3.0" 36 | libc = "0.2.51" 37 | locale = "0.2.2" 38 | log = "0.4.6" 39 | natord = "1.0.9" 40 | num_cpus = "1.10.0" 41 | number_prefix = "0.4" 42 | scoped_threadpool = "0.1.9" 43 | term_grid = "0.2" 44 | term_size = "0.3.1" 45 | unicode-width = "0.1.5" 46 | users = "0.11" 47 | zoneinfo_compiled = "0.5" 48 | 49 | [dependencies.git2] 50 | version = "0.14" 51 | optional = true 52 | default-features = false 53 | 54 | [build-dependencies] 55 | datetime = "0.5" 56 | 57 | [features] 58 | default = [ "git" ] 59 | git = [ "git2" ] 60 | vendored-openssl = ["git2/vendored-openssl"] 61 | 62 | [profile.release] 63 | opt-level = 3 64 | debug = false 65 | # lto = true 66 | panic = "abort" 67 | 68 | 69 | [package.metadata.deb] 70 | license-file = [ "LICENCE" ] 71 | depends = "$auto" 72 | extended-description = """ 73 | exa is a replacement for ls written in Rust. 74 | """ 75 | section = "utils" 76 | priority = "optional" 77 | assets = [ 78 | [ "target/release/exa", "/usr/bin/exa", "0755" ], 79 | [ "contrib/man/exa.1", "/usr/share/man/man1/exa.1", "0644" ], 80 | [ "contrib/completions.bash", "/etc/bash_completion.d/exa", "0644" ], 81 | ] 82 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Benjamin Sago 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR = 2 | PREFIX = /usr/local 3 | 4 | override define compdir 5 | ifndef $(1) 6 | $(1) := $$(or $$(shell pkg-config --variable=completionsdir $(2) 2>/dev/null),$(3)) 7 | endif 8 | endef 9 | 10 | $(eval $(call compdir,BASHDIR,bash-completion,$(PREFIX)/etc/bash_completion.d)) 11 | $(eval $(call compdir,ZSHDIR,zsh,/usr/share/zsh/vendor_completions.d)) 12 | $(eval $(call compdir,FISHDIR,fish,$(PREFIX)/share/fish/vendor_completions.d)) 13 | 14 | FEATURES ?= default 15 | CARGO_OPTS := --no-default-features --features "$(FEATURES)" 16 | 17 | all: target/release/exa 18 | build: target/release/exa 19 | 20 | target/release/exa: 21 | cargo build --release $(CARGO_OPTS) 22 | 23 | install: install-exa install-man 24 | 25 | install-exa: target/release/exa 26 | install -m755 -- target/release/exa "$(DESTDIR)$(PREFIX)/bin/" 27 | 28 | install-man: 29 | install -dm755 -- "$(DESTDIR)$(PREFIX)/bin/" "$(DESTDIR)$(PREFIX)/share/man/man1/" 30 | install -m644 -- contrib/man/exa.1 "$(DESTDIR)$(PREFIX)/share/man/man1/" 31 | 32 | install-bash-completions: 33 | install -m644 -- contrib/completions.bash "$(DESTDIR)$(BASHDIR)/exa" 34 | 35 | install-zsh-completions: 36 | install -m644 -- contrib/completions.zsh "$(DESTDIR)$(ZSHDIR)/_exa" 37 | 38 | install-fish-completions: 39 | install -m644 -- contrib/completions.fish "$(DESTDIR)$(FISHDIR)/exa.fish" 40 | 41 | test: target/release/exa 42 | cargo test --release $(CARGO_OPTS) 43 | 44 | check: test 45 | 46 | uninstall: 47 | -rm -f -- "$(DESTDIR)$(PREFIX)/share/man/man1/exa.1" 48 | -rm -f -- "$(DESTDIR)$(PREFIX)/bin/exa" 49 | -rm -f -- "$(DESTDIR)$(BASHDIR)/exa" 50 | -rm -f -- "$(DESTDIR)$(ZSHDIR)/_exa" 51 | -rm -f -- "$(DESTDIR)$(FISHDIR)/exa.fish" 52 | 53 | clean: 54 | cargo clean 55 | 56 | preview-man: 57 | man contrib/man/exa.1 58 | 59 | help: 60 | @echo 'Available make targets:' 61 | @echo ' all - build exa (default)' 62 | @echo ' build - build exa' 63 | @echo ' clean - run `cargo clean`' 64 | @echo ' install - build and install exa and manpage' 65 | @echo ' install-exa - build and install exa' 66 | @echo ' install-man - install the manpage' 67 | @echo ' test - run `cargo test`' 68 | @echo ' uninstall - uninstall fish, manpage, and completions' 69 | @echo ' preview-man - preview the manpage without installing' 70 | @echo ' help - print this help' 71 | @echo 72 | @echo ' install-bash-completions - install bash completions into $$BASHDIR' 73 | @echo ' install-zsh-completions - install zsh completions into $$ZSHDIR' 74 | @echo ' install-fish-completions - install fish completions into $$FISHDIR' 75 | @echo 76 | @echo 'Variables:' 77 | @echo ' DESTDIR - A path that'\''s prepended to installation paths (default: "")' 78 | @echo ' PREFIX - The installation prefix for everything except zsh completions (default: /usr/local)' 79 | @echo ' BASHDIR - The directory to install bash completions in (default: $$PREFIX/etc/bash_completion.d)' 80 | @echo ' ZSHDIR - The directory to install zsh completions in (default: /usr/share/zsh/vendor-completions)' 81 | @echo ' FISHDIR - The directory to install fish completions in (default: $$PREFIX/share/fish/vendor_completions.d)' 82 | @echo ' FEATURES - The cargo feature flags to use. Set to an empty string to disable git support' 83 | 84 | .PHONY: all build target/release/exa install-exa install-man preview-man \ 85 | install-bash-completions install-zsh-completions install-fish-completions \ 86 | clean uninstall help 87 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | /// The version string isn’t the simplest: we want to show the version, 2 | /// current Git hash, and compilation date when building *debug* versions, but 3 | /// just the version for *release* versions so the builds are reproducible. 4 | /// 5 | /// This script generates the string from the environment variables that Cargo 6 | /// adds (http://doc.crates.io/environment-variables.html) and runs `git` to 7 | /// get the SHA1 hash. It then writes the string into a file, which exa then 8 | /// includes at build-time. 9 | /// 10 | /// - https://stackoverflow.com/q/43753491/3484614 11 | /// - https://crates.io/crates/vergen 12 | extern crate datetime; 13 | 14 | use std::env; 15 | use std::io::Result as IOResult; 16 | 17 | fn git_hash() -> String { 18 | use std::process::Command; 19 | 20 | String::from_utf8_lossy( 21 | &Command::new("git") 22 | .args(&["rev-parse", "--short", "HEAD"]) 23 | .output() 24 | .unwrap() 25 | .stdout, 26 | ) 27 | .trim() 28 | .to_string() 29 | } 30 | 31 | fn main() { 32 | write_statics().unwrap(); 33 | } 34 | 35 | fn is_development_version() -> bool { 36 | // Both weekly releases and actual releases are --release releases, 37 | // but actual releases will have a proper version number 38 | cargo_version().ends_with("-pre") || env::var("PROFILE").unwrap() == "debug" 39 | } 40 | 41 | fn cargo_version() -> String { 42 | env::var("CARGO_PKG_VERSION").unwrap() 43 | } 44 | 45 | fn build_date() -> String { 46 | use datetime::{LocalDateTime, ISO}; 47 | 48 | let now = LocalDateTime::now(); 49 | format!("{}", now.date().iso()) 50 | } 51 | 52 | fn write_statics() -> IOResult<()> { 53 | use std::fs::File; 54 | use std::io::Write; 55 | use std::path::PathBuf; 56 | 57 | let ver = if is_development_version() { 58 | format!( 59 | "exa v{} ({} built on {})", 60 | cargo_version(), 61 | git_hash(), 62 | build_date() 63 | ) 64 | } else { 65 | format!("exa v{}", cargo_version()) 66 | }; 67 | 68 | let out = PathBuf::from(env::var("OUT_DIR").unwrap()); 69 | let mut f = File::create(&out.join("version_string.txt"))?; 70 | write!(f, "{:?}", ver) 71 | } 72 | -------------------------------------------------------------------------------- /contrib/completions.bash: -------------------------------------------------------------------------------- 1 | _exa() 2 | { 3 | cur=${COMP_WORDS[COMP_CWORD]} 4 | prev=${COMP_WORDS[COMP_CWORD-1]} 5 | 6 | case "$prev" in 7 | -'?'|--help|-v|--version) 8 | return 9 | ;; 10 | 11 | -L|--level) 12 | COMPREPLY=( $( compgen -W '{0..9}' -- "$cur" ) ) 13 | return 14 | ;; 15 | 16 | -s|--sort) 17 | COMPREPLY=( $( compgen -W 'name filename Name Filename size filesize extension Extension date time modified changed accessed created type inode oldest newest age none --' -- "$cur" ) ) 18 | return 19 | ;; 20 | 21 | -t|--time) 22 | COMPREPLY=( $( compgen -W 'modified changed accessed created --' -- $cur ) ) 23 | return 24 | ;; 25 | 26 | --time-style) 27 | COMPREPLY=( $( compgen -W 'default iso long-iso full-iso --' -- $cur ) ) 28 | return 29 | ;; 30 | esac 31 | 32 | case "$cur" in 33 | -*) 34 | COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) ) 35 | ;; 36 | 37 | *) 38 | _filedir 39 | ;; 40 | esac 41 | } && 42 | complete -o filenames -o bashdefault -F _exa exa 43 | -------------------------------------------------------------------------------- /contrib/completions.fish: -------------------------------------------------------------------------------- 1 | # Meta-stuff 2 | complete -c exa -s 'v' -l 'version' -d "Show version of exa" 3 | complete -c exa -s '?' -l 'help' -d "Show list of command-line options" 4 | 5 | # Display options 6 | complete -c exa -s '1' -l 'oneline' -d "Display one entry per line" 7 | complete -c exa -s 'l' -l 'long' -d "Display extended file metadata as a table" 8 | complete -c exa -s 'G' -l 'grid' -d "Display entries in a grid" 9 | complete -c exa -s 'x' -l 'across' -d "Sort the grid across, rather than downwards" 10 | complete -c exa -s 'R' -l 'recurse' -d "Recurse into directories" 11 | complete -c exa -s 'T' -l 'tree' -d "Recurse into directories as a tree" 12 | complete -c exa -s 'F' -l 'classify' -d "Display type indicator by file names" 13 | complete -c exa -l 'color' -d "When to use terminal colours" 14 | complete -c exa -l 'colour' -d "When to use terminal colours" 15 | complete -c exa -l 'color-scale' -d "Highlight levels of file sizes distinctly" 16 | complete -c exa -l 'colour-scale' -d "Highlight levels of file sizes distinctly" 17 | complete -c exa -l 'icons' -d "Display icons" 18 | 19 | # Filtering and sorting options 20 | complete -c exa -l 'group-directories-first' -d "Sort directories before other files" 21 | complete -c exa -l 'git-ignore' -d "Ignore files mentioned in '.gitignore'" 22 | complete -c exa -s 'a' -l 'all' -d "Show hidden and 'dot' files" 23 | complete -c exa -s 'd' -l 'list-dirs' -d "List directories like regular files" 24 | complete -c exa -s 'L' -l 'level' -d "Limit the depth of recursion" -a "1 2 3 4 5 6 7 8 9" 25 | complete -c exa -s 'r' -l 'reverse' -d "Reverse the sort order" 26 | complete -c exa -s 's' -l 'sort' -x -d "Which field to sort by" -a " 27 | accessed\t'Sort by file accessed time' 28 | age\t'Sort by file modified time (newest first)' 29 | changed\t'Sort by changed time' 30 | created\t'Sort by file modified time' 31 | date\t'Sort by file modified time' 32 | ext\t'Sort by file extension' 33 | Ext\t'Sort by file extension (uppercase first)' 34 | extension\t'Sort by file extension' 35 | Extension\t'Sort by file extension (uppercase first)' 36 | filename\t'Sort by filename' 37 | Filename\t'Sort by filename (uppercase first)' 38 | inode\t'Sort by file inode' 39 | modified\t'Sort by file modified time' 40 | name\t'Sort by filename' 41 | Name\t'Sort by filename (uppercase first)' 42 | newest\t'Sort by file modified time (newest first)' 43 | none\t'Do not sort files at all' 44 | oldest\t'Sort by file modified time' 45 | size\t'Sort by file size' 46 | time\t'Sort by file modified time' 47 | type\t'Sort by file type' 48 | " 49 | 50 | complete -c exa -s 'I' -l 'ignore-glob' -d "Ignore files that match these glob patterns" -r 51 | complete -c exa -s 'D' -l 'only-dirs' -d "List only directories" 52 | 53 | # Long view options 54 | complete -c exa -s 'b' -l 'binary' -d "List file sizes with binary prefixes" 55 | complete -c exa -s 'B' -l 'bytes' -d "List file sizes in bytes, without any prefixes" 56 | complete -c exa -s 'g' -l 'group' -d "List each file's group" 57 | complete -c exa -s 'h' -l 'header' -d "Add a header row to each column" 58 | complete -c exa -s 'h' -l 'links' -d "List each file's number of hard links" 59 | complete -c exa -s 'g' -l 'group' -d "List each file's inode number" 60 | complete -c exa -s 'S' -l 'blocks' -d "List each file's number of filesystem blocks" 61 | complete -c exa -s 't' -l 'time' -x -d "Which timestamp field to list" -a " 62 | modified\t'Display modified time' 63 | changed\t'Display changed time' 64 | accessed\t'Display accessed time' 65 | created\t'Display created time' 66 | " 67 | complete -c exa -s 'm' -l 'modified' -d "Use the modified timestamp field" 68 | complete -c exa -l 'changed' -d "Use the changed timestamp field" 69 | complete -c exa -s 'u' -l 'accessed' -d "Use the accessed timestamp field" 70 | complete -c exa -s 'U' -l 'created' -d "Use the created timestamp field" 71 | complete -c exa -l 'time-style' -x -d "How to format timestamps" -a " 72 | default\t'Use the default time style' 73 | iso\t'Display brief ISO timestamps' 74 | long-iso\t'Display longer ISO timestaps, up to the minute' 75 | full-iso\t'Display full ISO timestamps, up to the nanosecond' 76 | " 77 | complete -c exa -l 'no-permissions' -d "Suppress the permissions field" 78 | complete -c exa -l 'no-filesize' -d "Suppress the filesize field" 79 | complete -c exa -l 'no-user' -d "Suppress the user field" 80 | complete -c exa -l 'no-time' -d "Suppress the time field" 81 | 82 | # Optional extras 83 | complete -c exa -l 'git' -d "List each file's Git status, if tracked" 84 | complete -c exa -s '@' -l 'extended' -d "List each file's extended attributes and sizes" 85 | -------------------------------------------------------------------------------- /contrib/completions.zsh: -------------------------------------------------------------------------------- 1 | #compdef exa 2 | 3 | # Save this file as _exa in /usr/local/share/zsh/site-functions or in any 4 | # other folder in $fpath. E. g. save it in a folder called ~/.zfunc and add a 5 | # line containing `fpath=(~/.zfunc $fpath)` somewhere before `compinit` in your 6 | # ~/.zshrc. 7 | 8 | __exa() { 9 | # Give completions using the `_arguments` utility function with 10 | # `-s` for option stacking like `exa -ab` for `exa -a -b` and 11 | # `-S` for delimiting options with `--` like in `exa -- -a`. 12 | _arguments -s -S \ 13 | "(- *)"{-v,--version}"[Show version of exa]" \ 14 | "(- *)"{-'\?',--help}"[Show list of command-line options]" \ 15 | {-1,--oneline}"[Display one entry per line]" \ 16 | {-l,--long}"[Display extended file metadata as a table]" \ 17 | {-G,--grid}"[Display entries as a grid]" \ 18 | {-x,--across}"[Sort the grid across, rather than downwards]" \ 19 | {-R,--recurse}"[Recurse into directories]" \ 20 | {-T,--tree}"[Recurse into directories as a tree]" \ 21 | {-F,--classify}"[Display type indicator by file names]" \ 22 | --colo{,u}r"[When to use terminal colours]" \ 23 | --colo{,u}r-scale"[Highlight levels of file sizes distinctly]" \ 24 | --icons"[Display icons]" \ 25 | --group-directories-first"[Sort directories before other files]" \ 26 | --git-ignore"[Ignore files mentioned in '.gitignore']" \ 27 | {-a,--all}"[Show hidden and 'dot' files]" \ 28 | {-d,--list-dirs}"[List directories like regular files]" \ 29 | {-D,--only-dirs}"[List only directories]" \ 30 | {-L,--level}"+[Limit the depth of recursion]" \ 31 | {-r,--reverse}"[Reverse the sort order]" \ 32 | {-s,--sort}="[Which field to sort by]:(sort field):(accessed age changed created date extension Extension filename Filename inode modified oldest name Name newest none size time type)" \ 33 | {-I,--ignore-glob}"[Ignore files that match these glob patterns]" \ 34 | {-b,--binary}"[List file sizes with binary prefixes]" \ 35 | {-B,--bytes}"[List file sizes in bytes, without any prefixes]" \ 36 | --changed"[Use the changed timestamp field]" \ 37 | {-g,--group}"[List each file's group]" \ 38 | {-h,--header}"[Add a header row to each column]" \ 39 | {-H,--links}"[List each file's number of hard links]" \ 40 | {-i,--inode}"[List each file's inode number]" \ 41 | {-m,--modified}"[Use the modified timestamp field]" \ 42 | {-S,--blocks}"[List each file's number of filesystem blocks]" \ 43 | {-t,--time}="[Which time field to show]:(time field):(accessed changed created modified)" \ 44 | --time-style="[How to format timestamps]:(time style):(default iso long-iso full-iso)" \ 45 | --no-permissions"[Suppress the permissions field]" \ 46 | --no-filesize"[Suppress the filesize field]" \ 47 | --no-user"[Suppress the user field]" \ 48 | --no-time"[Suppress the time field]" \ 49 | {-u,--accessed}"[Use the accessed timestamp field]" \ 50 | {-U,--created}"[Use the created timestamp field]" \ 51 | --git"[List each file's Git status, if tracked]" \ 52 | {-@,--extended}"[List each file's extended attributes and sizes]" \ 53 | '*:filename:_files' 54 | } 55 | 56 | __exa 57 | -------------------------------------------------------------------------------- /devtools/README.md: -------------------------------------------------------------------------------- 1 | ## exa development tools 2 | 3 | These scripts deal with things like packaging release-worthy versions of exa and making sure the published versions actually work. 4 | 5 | They are **not general-purpose scripts** that you’re able to run from your main computer! They’re intended to be run from the Vagrant machines — they have commands such as ‘package-exa’ or ‘check-release’ that execute them instead. 6 | -------------------------------------------------------------------------------- /devtools/dev-bash.sh: -------------------------------------------------------------------------------- 1 | # This file gets executed when a user starts a `bash` shell, usually because 2 | # they’ve just started a new Vagrant session with `vagrant ssh`. It configures 3 | # some (but not all) of the commands that you can use. 4 | 5 | 6 | # Display the installed versions of tools. 7 | # help banner 8 | bash /vagrant/devtools/dev-versions.sh 9 | 10 | 11 | # Configure the Cool Prompt™ (not actually trademarked). 12 | # The Cool Prompt tells you whether you’re in debug or strict mode, whether 13 | # you have colours configured, and whether your last command failed. 14 | function nonzero_return() { RETVAL=$?; [ $RETVAL -ne 0 ] && echo "$RETVAL "; } 15 | function debug_mode() { [ -n "$EXA_DEBUG" ] && echo "debug "; } 16 | function strict_mode() { [ -n "$EXA_STRICT" ] && echo "strict "; } 17 | function lsc_mode() { [ -n "$LS_COLORS" ] && echo "lsc "; } 18 | function exac_mode() { [ -n "$EXA_COLORS" ] && echo "exac "; } 19 | 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\] " 20 | 21 | 22 | # The ‘debug’ function lets you switch debug mode on and off. 23 | # Turn it on if you need to see exa’s debugging logs. 24 | function debug () { 25 | case "$1" in "on") export EXA_DEBUG=1 ;; 26 | "off") export EXA_DEBUG= ;; 27 | "") [ -n "$EXA_DEBUG" ] && echo "debug on" || echo "debug off" ;; 28 | *) echo "Usage: debug on|off"; return 1 ;; esac; } 29 | 30 | # The ‘strict’ function lets you switch strict mode on and off. 31 | # Turn it on if you’d like exa’s command-line arguments checked. 32 | function strict () { 33 | case "$1" in "on") export EXA_STRICT=1 ;; 34 | "off") export EXA_STRICT= ;; 35 | "") [ -n "$EXA_STRICT" ] && echo "strict on" || echo "strict off" ;; 36 | *) echo "Usage: strict on|off"; return 1 ;; esac; } 37 | 38 | # The ‘colors’ function sets or unsets the ‘LS_COLORS’ and ‘EXA_COLORS’ 39 | # environment variables. There’s also a ‘hacker’ theme which turns everything 40 | # green, which is usually used for checking that all colour codes work, and 41 | # for looking cool while you phreak some mainframes or whatever. 42 | function colors () { 43 | case "$1" in 44 | "ls") 45 | 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" 46 | export EXA_COLORS="" ;; 47 | "hacker") 48 | export LS_COLORS="di=32:ex=32:fi=32:pi=32:so=32:bd=32:cd=32:ln=32:or=32:mi=32" 49 | export EXA_COLORS="ur=32:uw=32:ux=32:ue=32:gr=32:gw=32:gx=32:tr=32:tw=32:tx=32:su=32:sf=32:xa=32:sn=32:sb=32:df=32:ds=32:uu=32:un=32:gu=32:gn=32:lc=32:lm=32:ga=32:gm=32:gd=32:gv=32:gt=32:xx=32:da=32:in=32:bl=32:hd=32:lp=32:cc=32:" ;; 50 | "off") 51 | export LS_COLORS= 52 | export EXA_COLORS= ;; 53 | "") 54 | [ -n "$LS_COLORS" ] && echo "LS_COLORS=$LS_COLORS" || echo "ls-colors off" 55 | [ -n "$EXA_COLORS" ] && echo "EXA_COLORS=$EXA_COLORS" || echo "exa-colors off" ;; 56 | *) echo "Usage: ls-colors ls|hacker|off"; return 1 ;; esac; } 57 | -------------------------------------------------------------------------------- /devtools/dev-download-and-check-release.sh: -------------------------------------------------------------------------------- 1 | # This script downloads the published versions of exa from GitHub and my site, 2 | # checks that the checksums match, and makes sure the files at least unzip and 3 | # execute okay. 4 | # 5 | # The argument should be of the form “0.8.0”, no ‘v’. That version was the 6 | # first one to offer checksums, so it’s the minimum version that can be tested. 7 | 8 | set +x 9 | trap 'exit' ERR 10 | 11 | exa_version=$1 12 | if [[ -z "$exa_version" ]]; then 13 | echo "Please specify a version, such as '$0 0.8.0'" 14 | exit 1 15 | fi 16 | 17 | 18 | # Delete anything that already exists 19 | rm -rfv "/tmp/${exa_version}-downloads" 20 | 21 | 22 | # Create a temporary directory and download exa into it 23 | mkdir "/tmp/${exa_version}-downloads" 24 | cd "/tmp/${exa_version}-downloads" 25 | 26 | echo -e "\n\033[4mDownloading stuff...\033[0m" 27 | wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/exa-macos-x86_64-${exa_version}.zip" 28 | wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/exa-linux-x86_64-${exa_version}.zip" 29 | 30 | wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/MD5SUMS" 31 | wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/SHA1SUMS" 32 | 33 | 34 | # Unzip the zips and check the sums 35 | echo -e "\n\033[4mExtracting that stuff...\033[0m" 36 | unzip "exa-macos-x86_64-${exa_version}.zip" 37 | unzip "exa-linux-x86_64-${exa_version}.zip" 38 | 39 | echo -e "\n\033[4mValidating MD5 checksums...\033[0m" 40 | md5sum -c MD5SUMS 41 | 42 | echo -e "\n\033[4mValidating SHA1 checksums...\033[0m" 43 | sha1sum -c SHA1SUMS 44 | 45 | 46 | # Finally, give the Linux version a go 47 | echo -e "\n\033[4mChecking it actually runs...\033[0m" 48 | ./"exa-linux-x86_64" --version 49 | ./"exa-linux-x86_64" --long 50 | 51 | echo -e "\n\033[1;32mAll's lookin' good!\033[0m" 52 | -------------------------------------------------------------------------------- /devtools/dev-generate-checksums.sh: -------------------------------------------------------------------------------- 1 | # This script generates the MD5SUMS and SHA1SUMS files. 2 | # You’ll need to have run ‘dev-download-and-check-release.sh’ and 3 | # ‘local-package-for-macos.sh’ scripts to generate the binaries first. 4 | 5 | set +x 6 | trap 'exit' ERR 7 | 8 | cd /vagrant 9 | rm -f MD5SUMS SHA1SUMS 10 | 11 | echo -e "\n\033[4mValidating MD5 checksums...\033[0m" 12 | md5sum exa-linux-x86_64 exa-macos-x86_64 | tee MD5SUMS 13 | 14 | echo -e "\n\033[4mValidating SHA1 checksums...\033[0m" 15 | sha1sum exa-linux-x86_64 exa-macos-x86_64 | tee SHA1SUMS 16 | -------------------------------------------------------------------------------- /devtools/dev-help-testvm.sh: -------------------------------------------------------------------------------- 1 | # This file is like the other one, except for the testing VM. 2 | # It also gets dumped into /etc/motd. 3 | 4 | 5 | echo -e " 6 | \033[1;33mThe exa testing environment!\033[0m 7 | This machine is dependency-free, and can be used to test that 8 | released versions of exa still work on vanilla Linux installs. 9 | 10 | \033[4mCommands\033[0m 11 | \033[32;1mcheck-release\033[0m to download and verify released binaries 12 | " 13 | -------------------------------------------------------------------------------- /devtools/dev-help.sh: -------------------------------------------------------------------------------- 1 | # This file prints out some help text that says which commands are available 2 | # in the VM. It gets executed during Vagrant provisioning and its output gets 3 | # dumped into /etc/motd, to print it when a user starts a new Vagrant session. 4 | 5 | 6 | echo -e " 7 | \033[1;33mThe exa development environment!\033[0m 8 | exa's source is available at \033[33m/vagrant\033[0m. 9 | Binaries get built into \033[33m/home/vagrant/target\033[0m. 10 | 11 | \033[4mCommands\033[0m 12 | \033[32;1mexa\033[0m to run the built version of exa 13 | \033[32;1mbuild-exa\033[0m (or \033[32;1mb\033[0m) to run \033[1mcargo build\033[0m 14 | \033[32;1mtest-exa\033[0m (or \033[32;1mt\033[0m) to run \033[1mcargo test\033[0m 15 | \033[32;1mrun-xtests\033[0m (or \033[32;1mx\033[0m) to run the extended tests 16 | \033[32;1mcompile-exa\033[0m (or \033[32;1mc\033[0m) to run the above three 17 | \033[32;1mdebug\033[0m to toggle printing logs 18 | \033[32;1mstrict\033[0m to toggle strict mode 19 | \033[32;1mcolors\033[0m to toggle custom colours 20 | \033[32;1mhalp\033[0m to show all this again 21 | " 22 | -------------------------------------------------------------------------------- /devtools/dev-package-for-linux.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | # This script builds a publishable release-worthy version of exa. 4 | # It gets the version number, builds exa using cargo, tests it, strips the 5 | # binary, compresses it into a zip, then puts it in /vagrant so it’s 6 | # accessible from the host machine. 7 | # 8 | # If you’re in the VM, you can run it using the ‘package-exa’ command. 9 | 10 | 11 | # Linux check! 12 | uname=`uname -s` 13 | if [[ "$uname" != "Linux" ]]; then 14 | echo "Gotta be on Linux to run this (detected '$uname')!" 15 | exit 1 16 | fi 17 | 18 | # First, we need to get the version number to figure out what to call the zip. 19 | # We do this by getting the first line from the Cargo.toml that matches 20 | # /version/, removing its whitespace, and building a command out of it, so the 21 | # shell executes something like `exa_version="0.8.0"`, which it understands as 22 | # a variable definition. Hey, it’s not a hack if it works. 23 | toml_file="/vagrant/Cargo.toml" 24 | eval exa_$(grep version $toml_file | head -n 1 | sed "s/ //g") 25 | if [ -z "$exa_version" ]; then 26 | echo "Failed to parse version number! Can't build exa!" 27 | exit 1 28 | fi 29 | 30 | # Weekly builds have a bit more information in their version number (see build.rs). 31 | if [[ "$1" == "--weekly" ]]; then 32 | git_hash=`GIT_DIR=/vagrant/.git git rev-parse --short --verify HEAD` 33 | date=`date +"%Y-%m-%d"` 34 | echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash" 35 | else 36 | echo "Building exa v$exa_version" 37 | fi 38 | 39 | # Compilation is done in --release mode, which takes longer but produces a 40 | # faster binary. This binary gets built to a different place, so the extended 41 | # tests script needs to be told which one to use. 42 | echo -e "\n\033[4mCompiling release version of exa...\033[0m" 43 | exa_linux_binary="/vagrant/exa-linux-x86_64" 44 | rm -vf "$exa_linux_binary" 45 | cargo build --release --manifest-path "$toml_file" 46 | cargo test --release --manifest-path "$toml_file" --lib -- --quiet 47 | /vagrant/xtests/run.sh --release 48 | cp /home/vagrant/target/release/exa "$exa_linux_binary" 49 | 50 | # Stripping the binary before distributing it removes a bunch of debugging 51 | # symbols, saving some space. 52 | echo -e "\n\033[4mStripping binary...\033[0m" 53 | strip -v "$exa_linux_binary" 54 | 55 | # Compress the binary for upload. The ‘-j’ flag is necessary to avoid the 56 | # /vagrant path being in the zip too. Only the zip gets the version number, so 57 | # the binaries can have consistent names, and it’s still possible to tell 58 | # different *downloads* apart. 59 | echo -e "\n\033[4mZipping binary...\033[0m" 60 | if [[ "$1" == "--weekly" ]] 61 | then exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}-${date}-${git_hash}.zip" 62 | else exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}.zip" 63 | fi 64 | rm -vf "$exa_linux_zip" 65 | zip -j "$exa_linux_zip" "$exa_linux_binary" 66 | 67 | # There was a problem a while back where a library was getting unknowingly 68 | # *dynamically* linked, which broke the whole ‘self-contained binary’ concept. 69 | # So dump the linker table, in case anything unscrupulous shows up. 70 | echo -e "\n\033[4mLibraries linked:\033[0m" 71 | ldd "$exa_linux_binary" | sed "s/\t//" 72 | 73 | # Might as well use it to test itself, right? 74 | echo -e "\n\033[4mAll done! Files produced:\033[0m" 75 | "$exa_linux_binary" "$exa_linux_binary" "$exa_linux_zip" -lB 76 | -------------------------------------------------------------------------------- /devtools/dev-run-debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ -f ~/target/debug/exa ]]; then 3 | ~/target/debug/exa "$@" 4 | else 5 | echo -e "Debug exa binary does not exist!" 6 | echo -e "Run \033[32;1mb\033[0m or \033[32;1mbuild-exa\033[0m to create it" 7 | fi 8 | -------------------------------------------------------------------------------- /devtools/dev-run-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ -f ~/target/release/exa ]]; then 3 | ~/target/release/exa "$@" 4 | else 5 | echo -e "Release exa binary does not exist!" 6 | echo -e "Run \033[32;1mb --release\033[0m or \033[32;1mbuild-exa --release\033[0m to create it" 7 | fi 8 | -------------------------------------------------------------------------------- /devtools/dev-versions.sh: -------------------------------------------------------------------------------- 1 | # Displays the installed versions of Rust and Cargo. 2 | # This gets run from ‘dev-bash.sh’, which gets run from ‘~/.bash_profile’, so 3 | # the versions gets displayed after the help text for a new Vagrant session. 4 | 5 | echo -e "\\033[4mVersions\\033[0m" 6 | rustc --version 7 | cargo --version 8 | echo 9 | -------------------------------------------------------------------------------- /devtools/local-package-for-macos.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | # This script builds a publishable release-worthy version of exa. 4 | # It gets the version number, builds exa using cargo, tests it, strips the 5 | # binary, and compresses it into a zip. 6 | # 7 | # It’s *mostly* the same as dev-package-for-linux.sh, except with some 8 | # Mach-specific things (otool instead of ldd), BSD-coreutils-specific things, 9 | # and it doesn’t run the xtests. 10 | 11 | 12 | # Virtualising macOS is a legal minefield, so this script is ‘local’ instead 13 | # of ‘dev’: I run it from my actual machine, rather than from a VM. 14 | uname=`uname -s` 15 | if [[ "$uname" != "Darwin" ]]; then 16 | echo "Gotta be on Darwin to run this (detected '$uname')!" 17 | exit 1 18 | fi 19 | 20 | # First, we need to get the version number to figure out what to call the zip. 21 | # We do this by getting the first line from the Cargo.toml that matches 22 | # /version/, removing its whitespace, and building a command out of it, so the 23 | # shell executes something like `exa_version="0.8.0"`, which it understands as 24 | # a variable definition. Hey, it’s not a hack if it works. 25 | # 26 | # Because this can’t use the absolute /vagrant path, this has to use what this 27 | # SO answer calls a “quoting disaster”: https://stackoverflow.com/a/20196098/3484614 28 | # You will also need GNU coreutils: https://stackoverflow.com/a/4031502/3484614 29 | exa_root="$(dirname "$(dirname "$(greadlink -fm "$0")")")" 30 | toml_file="$exa_root"/Cargo.toml 31 | eval exa_$(grep version $toml_file | head -n 1 | sed "s/ //g") 32 | if [ -z "$exa_version" ]; then 33 | echo "Failed to parse version number! Can't build exa!" 34 | exit 1 35 | fi 36 | 37 | # Weekly builds have a bit more information in their version number (see build.rs). 38 | if [[ "$1" == "--weekly" ]]; then 39 | git_hash=`GIT_DIR=$exa_root/.git git rev-parse --short --verify HEAD` 40 | date=`date +"%Y-%m-%d"` 41 | echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash" 42 | else 43 | echo "Building exa v$exa_version" 44 | fi 45 | 46 | # Compilation is done in --release mode, which takes longer but produces a 47 | # faster binary. 48 | echo -e "\n\033[4mCompiling release version of exa...\033[0m" 49 | exa_macos_binary="$exa_root/exa-macos-x86_64" 50 | rm -vf "$exa_macos_binary" | sed 's/^/removing /' 51 | cargo build --release --manifest-path "$toml_file" 52 | cargo test --release --manifest-path "$toml_file" --lib -- --quiet 53 | # we can’t run the xtests outside the VM! 54 | #/vagrant/xtests/run.sh --release 55 | cp "$exa_root"/target/release/exa "$exa_macos_binary" 56 | 57 | # Stripping the binary before distributing it removes a bunch of debugging 58 | # symbols, saving some space. 59 | echo -e "\n\033[4mStripping binary...\033[0m" 60 | strip "$exa_macos_binary" 61 | echo "strip $exa_macos_binary" 62 | 63 | # Compress the binary for upload. The ‘-j’ flag is necessary to avoid the 64 | # current path being in the zip too. Only the zip gets the version number, so 65 | # the binaries can have consistent names, and it’s still possible to tell 66 | # different *downloads* apart. 67 | echo -e "\n\033[4mZipping binary...\033[0m" 68 | if [[ "$1" == "--weekly" ]] 69 | then exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}-${date}-${git_hash}.zip" 70 | else exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}.zip" 71 | fi 72 | rm -vf "$exa_macos_zip" | sed 's/^/removing /' 73 | zip -j "$exa_macos_zip" "$exa_macos_binary" 74 | 75 | # There was a problem a while back where a library was getting unknowingly 76 | # *dynamically* linked, which broke the whole ‘self-contained binary’ concept. 77 | # So dump the linker table, in case anything unscrupulous shows up. 78 | echo -e "\n\033[4mLibraries linked:\033[0m" 79 | otool -L "$exa_macos_binary" | sed 's/^[[:space:]]*//' 80 | 81 | # Might as well use it to test itself, right? 82 | echo -e "\n\033[4mAll done! Files produced:\033[0m" 83 | "$exa_macos_binary" "$exa_macos_binary" "$exa_macos_zip" -lB 84 | -------------------------------------------------------------------------------- /screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lespea/exa/a189c59425e9217c15a86fb0f3612dba54cc9dab/screenshots.png -------------------------------------------------------------------------------- /snap/.gitignore: -------------------------------------------------------------------------------- 1 | .snapcraft 2 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: exa 2 | version: 'latest' 3 | summary: Replacement for 'ls' written in Rust 4 | description: | 5 | It uses colours for information by default, helping you distinguish between 6 | many types of files, such as whether you are the owner, or in the owning 7 | group. It also has extra features not present in the original ls, such as 8 | viewing the Git status for a directory, or recursing into directories with a 9 | tree view. exa is written in Rust, so it’s small, fast, and portable. 10 | 11 | grade: stable 12 | confinement: classic 13 | 14 | apps: 15 | exa: 16 | command: exa 17 | 18 | parts: 19 | exa: 20 | plugin: rust 21 | source: . 22 | stage-packages: 23 | - libgit2-24 24 | - cmake 25 | - libz-dev 26 | -------------------------------------------------------------------------------- /src/bin/main.rs: -------------------------------------------------------------------------------- 1 | extern crate exa; 2 | extern crate libc; 3 | 4 | use std::env::{args_os, var_os}; 5 | use std::ffi::OsString; 6 | use std::io::{stderr, stdout, ErrorKind, Write}; 7 | use std::process::exit; 8 | 9 | use exa::Exa; 10 | 11 | fn main() { 12 | configure_logger(); 13 | 14 | let args: Vec = args_os().skip(1).collect(); 15 | match Exa::from_args(args.iter(), &mut stdout()) { 16 | Ok(mut exa) => { 17 | match exa.run() { 18 | Ok(exit_status) => exit(exit_status), 19 | Err(e) => { 20 | match e.kind() { 21 | ErrorKind::BrokenPipe => exit(exits::SUCCESS), 22 | _ => { 23 | eprintln!("{}", e); 24 | exit(exits::RUNTIME_ERROR); 25 | } 26 | }; 27 | } 28 | }; 29 | } 30 | 31 | Err(ref e) if e.is_error() => { 32 | let mut stderr = stderr(); 33 | writeln!(stderr, "{}", e).unwrap(); 34 | 35 | if let Some(s) = e.suggestion() { 36 | let _ = writeln!(stderr, "{}", s); 37 | } 38 | 39 | exit(exits::OPTIONS_ERROR); 40 | } 41 | 42 | Err(ref e) => { 43 | println!("{}", e); 44 | exit(exits::SUCCESS); 45 | } 46 | }; 47 | } 48 | 49 | /// Sets up a global logger if one is asked for. 50 | /// The ‘EXA_DEBUG’ environment variable controls whether log messages are 51 | /// displayed or not. Currently there are just two settings (on and off). 52 | /// 53 | /// This can’t be done in exa’s own option parsing because that part of it 54 | /// logs as well, so by the time execution gets there, the logger needs to 55 | /// have already been set up. 56 | pub fn configure_logger() { 57 | extern crate env_logger; 58 | extern crate log; 59 | 60 | let present = match var_os(exa::vars::EXA_DEBUG) { 61 | Some(debug) => debug.len() > 0, 62 | None => false, 63 | }; 64 | 65 | let mut logs = env_logger::Builder::new(); 66 | if present { 67 | logs.filter(None, log::LevelFilter::Debug); 68 | } else { 69 | logs.filter(None, log::LevelFilter::Off); 70 | } 71 | 72 | logs.init() 73 | } 74 | 75 | #[allow(trivial_numeric_casts)] 76 | mod exits { 77 | use libc::{self, c_int}; 78 | 79 | pub const SUCCESS: c_int = libc::EXIT_SUCCESS; 80 | pub const RUNTIME_ERROR: c_int = libc::EXIT_FAILURE; 81 | pub const OPTIONS_ERROR: c_int = 3 as c_int; 82 | } 83 | -------------------------------------------------------------------------------- /src/fs/dir_action.rs: -------------------------------------------------------------------------------- 1 | //! What to do when encountering a directory? 2 | 3 | /// The action to take when trying to list a file that turns out to be a 4 | /// directory. 5 | /// 6 | /// By default, exa will display the information about files passed in as 7 | /// command-line arguments, with one file per entry. However, if a directory 8 | /// is passed in, exa assumes that the user wants to see its contents, rather 9 | /// than the directory itself. 10 | /// 11 | /// This can get annoying sometimes: if a user does `exa ~/Downloads/img-*` 12 | /// to see the details of every file starting with `img-`, any directories 13 | /// that happen to start with the same will be listed after the files at 14 | /// the end in a separate block. By listing directories as files, their 15 | /// directory status will be ignored, and both will be listed side-by-side. 16 | /// 17 | /// These two modes have recursive analogues in the “recurse” and “tree” 18 | /// modes. Here, instead of just listing the directories, exa will descend 19 | /// into them and print out their contents. The recurse mode does this by 20 | /// having extra output blocks at the end, while the tree mode will show 21 | /// directories inline, with their contents immediately underneath. 22 | #[derive(PartialEq, Debug, Copy, Clone)] 23 | pub enum DirAction { 24 | /// This directory should be listed along with the regular files, instead 25 | /// of having its contents queried. 26 | AsFile, 27 | 28 | /// This directory should not be listed, and should instead be opened and 29 | /// *its* files listed separately. This is the default behaviour. 30 | List, 31 | 32 | /// This directory should be listed along with the regular files, and then 33 | /// its contents should be listed afterward. The recursive contents of 34 | /// *those* contents are dictated by the options argument. 35 | Recurse(RecurseOptions), 36 | } 37 | 38 | impl DirAction { 39 | /// Gets the recurse options, if this dir action has any. 40 | pub fn recurse_options(&self) -> Option { 41 | match *self { 42 | DirAction::Recurse(o) => Some(o), 43 | _ => None, 44 | } 45 | } 46 | 47 | /// Whether to treat directories as regular files or not. 48 | pub fn treat_dirs_as_files(&self) -> bool { 49 | match *self { 50 | DirAction::AsFile => true, 51 | DirAction::Recurse(o) => o.tree, 52 | _ => false, 53 | } 54 | } 55 | } 56 | 57 | /// The options that determine how to recurse into a directory. 58 | #[derive(PartialEq, Debug, Copy, Clone)] 59 | pub struct RecurseOptions { 60 | /// Whether recursion should be done as a tree or as multiple individual 61 | /// views of files. 62 | pub tree: bool, 63 | 64 | /// The maximum number of times that recursion should descend to, if one 65 | /// is specified. 66 | pub max_depth: Option, 67 | } 68 | 69 | impl RecurseOptions { 70 | /// Returns whether a directory of the given depth would be too deep. 71 | pub fn is_too_deep(&self, depth: usize) -> bool { 72 | match self.max_depth { 73 | None => false, 74 | Some(d) => d <= depth, 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/fs/feature/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod xattr; 2 | 3 | #[cfg(feature = "git")] 4 | pub mod git; 5 | 6 | #[cfg(not(feature = "git"))] 7 | pub mod git { 8 | use std::iter::FromIterator; 9 | use std::path::{Path, PathBuf}; 10 | 11 | use crate::fs::fields as f; 12 | 13 | pub struct GitCache; 14 | 15 | impl FromIterator for GitCache { 16 | fn from_iter>(_iter: I) -> Self { 17 | GitCache 18 | } 19 | } 20 | 21 | impl GitCache { 22 | pub fn has_anything_for(&self, _index: &Path) -> bool { 23 | false 24 | } 25 | 26 | pub fn get(&self, _index: &Path, _prefix_lookup: bool) -> f::Git { 27 | panic!("Tried to query a Git cache, but Git support is disabled") 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/fs/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::dir::{Dir, DotFilter}; 2 | pub use self::file::{File, FileTarget}; 3 | 4 | mod dir; 5 | pub mod dir_action; 6 | pub mod feature; 7 | pub mod fields; 8 | mod file; 9 | pub mod filter; 10 | -------------------------------------------------------------------------------- /src/info/filetype.rs: -------------------------------------------------------------------------------- 1 | //! Tests for various types of file (video, image, compressed, etc). 2 | //! 3 | //! Currently this is dependent on the file’s name and extension, because 4 | //! those are the only metadata that we have access to without reading the 5 | //! file’s contents. 6 | 7 | use ansi_term::Style; 8 | 9 | use crate::fs::File; 10 | use crate::output::file_name::FileColours; 11 | use crate::output::icons::FileIcon; 12 | 13 | #[derive(Debug, Default, PartialEq)] 14 | pub struct FileExtensions; 15 | 16 | impl FileExtensions { 17 | /// An “immediate” file is something that can be run or activated somehow 18 | /// in order to kick off the build of a project. It’s usually only present 19 | /// in directories full of source code. 20 | fn is_immediate(&self, file: &File) -> bool { 21 | file.name.to_lowercase().starts_with("readme") 22 | || file.name.ends_with(".ninja") 23 | || file.name_is_one_of(&[ 24 | "Makefile", 25 | "Cargo.toml", 26 | "SConstruct", 27 | "CMakeLists.txt", 28 | "build.gradle", 29 | "pom.xml", 30 | "Rakefile", 31 | "package.json", 32 | "Gruntfile.js", 33 | "Gruntfile.coffee", 34 | "BUILD", 35 | "BUILD.bazel", 36 | "WORKSPACE", 37 | "build.xml", 38 | "webpack.config.js", 39 | "meson.build", 40 | ]) 41 | } 42 | 43 | fn is_image(&self, file: &File) -> bool { 44 | file.extension_is_one_of(&[ 45 | "png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif", "ppm", "pgm", "pbm", "pnm", "webp", 46 | "raw", "arw", "svg", "stl", "eps", "dvi", "ps", "cbr", "jpf", "cbz", "xpm", "ico", 47 | "cr2", "orf", "nef", 48 | ]) 49 | } 50 | 51 | fn is_video(&self, file: &File) -> bool { 52 | file.extension_is_one_of(&[ 53 | "avi", "flv", "m2v", "m4v", "mkv", "mov", "mp4", "mpeg", "mpg", "ogm", "ogv", "vob", 54 | "wmv", "webm", "m2ts", 55 | ]) 56 | } 57 | 58 | fn is_music(&self, file: &File) -> bool { 59 | file.extension_is_one_of(&["aac", "m4a", "mp3", "ogg", "wma", "mka", "opus"]) 60 | } 61 | 62 | // Lossless music, rather than any other kind of data... 63 | fn is_lossless(&self, file: &File) -> bool { 64 | file.extension_is_one_of(&["alac", "ape", "flac", "wav"]) 65 | } 66 | 67 | fn is_crypto(&self, file: &File) -> bool { 68 | file.extension_is_one_of(&["asc", "enc", "gpg", "pgp", "sig", "signature", "pfx", "p12"]) 69 | } 70 | 71 | fn is_document(&self, file: &File) -> bool { 72 | file.extension_is_one_of(&[ 73 | "djvu", "doc", "docx", "dvi", "eml", "eps", "fotd", "odp", "odt", "pdf", "ppt", "pptx", 74 | "rtf", "xls", "xlsx", 75 | ]) 76 | } 77 | 78 | fn is_compressed(&self, file: &File) -> bool { 79 | file.extension_is_one_of(&[ 80 | "zip", "tar", "Z", "z", "gz", "bz2", "a", "ar", "7z", "iso", "dmg", "tc", "rar", "par", 81 | "tgz", "xz", "txz", "lz", "tlz", "lzma", "deb", "rpm", "zst", 82 | ]) 83 | } 84 | 85 | fn is_temp(&self, file: &File) -> bool { 86 | file.name.ends_with('~') 87 | || (file.name.starts_with('#') && file.name.ends_with('#')) 88 | || file.extension_is_one_of(&["tmp", "swp", "swo", "swn", "bak", "bk"]) 89 | } 90 | 91 | fn is_compiled(&self, file: &File) -> bool { 92 | if file.extension_is_one_of(&["class", "elc", "hi", "o", "pyc", "zwc"]) { 93 | true 94 | } else if let Some(dir) = file.parent_dir { 95 | file.get_source_files() 96 | .iter() 97 | .any(|path| dir.contains(path)) 98 | } else { 99 | false 100 | } 101 | } 102 | } 103 | 104 | impl FileColours for FileExtensions { 105 | fn colour_file(&self, file: &File) -> Option