Răsfoiți Sursa

Merge branch 'master' into makefile

Benjamin Sago 8 ani în urmă
părinte
comite
20b6588552

+ 14 - 97
Cargo.lock

@@ -8,7 +8,7 @@ dependencies = [
  "git2 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
  "locale 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "num_cpus 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -43,26 +43,13 @@ dependencies = [
  "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
-[[package]]
-name = "curl-sys"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
- "libz-sys 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
- "openssl-sys 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
 [[package]]
 name = "datetime"
 version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "iso8601 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
  "locale 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -73,15 +60,6 @@ name = "gcc"
 version = "0.3.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
-[[package]]
-name = "gdi32-sys"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
 [[package]]
 name = "getopts"
 version = "0.2.14"
@@ -93,10 +71,8 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
- "libgit2-sys 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "openssl-probe 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "openssl-sys 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libgit2-sys 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -130,33 +106,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "libc"
-version = "0.2.21"
+version = "0.2.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "libgit2-sys"
-version = "0.6.8"
+version = "0.6.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "cmake 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)",
- "curl-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
- "libssh2-sys 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
  "libz-sys 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
- "openssl-sys 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "libssh2-sys"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "cmake 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
- "libz-sys 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
- "openssl-sys 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -166,7 +127,7 @@ version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -175,7 +136,7 @@ name = "locale"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
  "num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -265,7 +226,7 @@ name = "num_cpus"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -276,23 +237,6 @@ dependencies = [
  "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
-[[package]]
-name = "openssl-probe"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "openssl-sys"
-version = "0.9.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)",
- "gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
- "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
 [[package]]
 name = "pad"
 version = "0.1.4"
@@ -311,7 +255,7 @@ name = "rand"
 version = "0.3.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -359,33 +303,14 @@ dependencies = [
  "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
-[[package]]
-name = "user32-sys"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
 [[package]]
 name = "users"
 version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
-[[package]]
-name = "winapi"
-version = "0.2.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
-name = "winapi-build"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
 [[package]]
 name = "zoneinfo_compiled"
 version = "0.2.1"
@@ -400,19 +325,16 @@ dependencies = [
 "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
 "checksum byteorder 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "96c8b41881888cc08af32d47ac4edd52bc7fa27fef774be47a92443756451304"
 "checksum cmake 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "d18d68987ed4c516dcc3e7913659bfa4076f5182eea4a7e0038bb060953e76ac"
-"checksum curl-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "23e7e544dc5e1ba42c4a4a678bd47985e84b9c3f4d3404c29700622a029db9c3"
 "checksum datetime 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2d425bf1f6bbd57cf833081c1e60ac294fd74e7edd66acc91c3fca2e496bcee9"
 "checksum gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)" = "40899336fb50db0c78710f53e87afc54d8c7266fb76262fecc78ca1a7f09deae"
-"checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518"
 "checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685"
 "checksum git2 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "046ae03385257040b2a35e56d9669d950dd911ba2bf48202fbef73ee6aab27b2"
 "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
 "checksum idna 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6ac85ec3f80c8e4e99d9325521337e14ec7555c458a14e377d189659a427f375"
 "checksum iso8601 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "11dc464f8c6f17595d191447c9c6559298b2d023d6f846a4a23ac7ea3c46c477"
 "checksum lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3b37545ab726dd833ec6420aaba8231c5b320814b9029ad585555d2a03e94fbf"
-"checksum libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "88ee81885f9f04bff991e306fea7c1c60a5f0f9e409e99f6b40e3311a3363135"
-"checksum libgit2-sys 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "68bed1d1198da5d2b047af68fd71613ddfaa3d5b68797181b33e9d8547263b4b"
-"checksum libssh2-sys 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "91e135645c2e198a39552c8c7686bb5b83b1b99f64831c040a6c2798a1195934"
+"checksum libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)" = "babb8281da88cba992fa1f4ddec7d63ed96280a1a53ec9b919fd37b53d71e502"
+"checksum libgit2-sys 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "a3aaa20337a0e79fb75180b6a1970c1f7cff9a413f570d6b999b38a5d5d54e81"
 "checksum libz-sys 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e5ee912a45d686d393d5ac87fac15ba0ba18daae14e8e7543c63ebf7fb7e970c"
 "checksum locale 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ecccf5186e43f84e543bbf61fcddf00b41d69d97093bc8989cc0cf1593681950"
 "checksum matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efd7622e3022e1a6eaa602c4cea8912254e5582c9c692e9167714182244801b1"
@@ -427,8 +349,6 @@ dependencies = [
 "checksum num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "e1cbfa3781f3fe73dc05321bed52a06d2d491eaa764c52335cf4399f046ece99"
 "checksum num_cpus 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca313f1862c7ec3e0dfe8ace9fa91b1d9cb5c84ace3d00f5ec4216238e93c167"
 "checksum number_prefix 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "59a14be9c211cb9c602bad35ac99f41e9a84b44d71b8cbd3040e3bd02a214902"
-"checksum openssl-probe 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d98df0270d404ccd3c050a41d579c52d1db15375168bb3471e04ec0f5f378daf"
-"checksum openssl-sys 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e5e0fd64cb2fa018ed2e7b2c8d9649114fe5da957c9a67432957f01e5dcc82e9"
 "checksum pad 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d1bf3336e626b898e7263790d432a711d4277e22faea20dd9f70e0cab268fa58"
 "checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903"
 "checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d"
@@ -439,8 +359,5 @@ dependencies = [
 "checksum unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e28fa37426fceeb5cf8f41ee273faa7c82c47dc8fba5853402841e665fcd86ff"
 "checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
 "checksum url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f5ba8a749fb4479b043733416c244fa9d1d3af3d7c23804944651c8a448cb87e"
-"checksum user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ef4711d107b21b410a3a974b1204d9accc8b10dad75d8324b5d755de1617d47"
 "checksum users 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7ae8fdf783cb9652109c99886459648feb92ecc749e6b8e7930f6decba74c7c"
-"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
-"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
 "checksum zoneinfo_compiled 0.2.1 (git+https://github.com/rust-datetime/zoneinfo-compiled.git)" = "<none>"

+ 1 - 0
Cargo.toml

@@ -39,6 +39,7 @@ lto = true
 [dependencies.git2]
 version = "0.6.4"
 optional = true
+default-features = false
 
 [dependencies.zoneinfo_compiled]
 git = "https://github.com/rust-datetime/zoneinfo-compiled.git"

+ 60 - 22
Makefile

@@ -1,25 +1,63 @@
-PREFIX ?= /usr/local
-
-BUILD = target/release/exa
-
-$(BUILD):
-	@which rustc > /dev/null || { echo "exa requires Rust to compile. For installation instructions, please visit http://rust-lang.org/"; exit 1; }
-	cargo build --release
-
-build: $(BUILD)
-
-build-no-git:
-	@which rustc > /dev/null || { echo "exa requires Rust to compile. For installation instructions, please visit http://rust-lang.org/"; exit 1; }
-	cargo build --release --no-default-features
-
-INSTALL = $(PREFIX)/bin/exa
-
-$(INSTALL):
+SRC = \
+	src/info/sources.rs \
+	src/info/mod.rs \
+	src/info/filetype.rs \
+	src/bin/main.rs \
+	src/term.rs \
+	src/exa.rs \
+	src/output/grid_details.rs \
+	src/output/tree.rs \
+	src/output/colours.rs \
+	src/output/grid.rs \
+	src/output/cell.rs \
+	src/output/mod.rs \
+	src/output/details.rs \
+	src/output/lines.rs \
+	src/output/column.rs \
+	src/fs/file.rs \
+	src/fs/fields.rs \
+	src/fs/mod.rs \
+	src/fs/dir.rs \
+	src/fs/feature/xattr.rs \
+	src/fs/feature/git.rs \
+	src/fs/feature/mod.rs \
+	src/options/misfire.rs \
+	src/options/filter.rs \
+	src/options/dir_action.rs \
+	src/options/view.rs \
+	src/options/mod.rs \
+	src/options/help.rs
+
+PREFIX = /usr/local
+
+CARGOFLAGS = --no-default-features
+
+all: target/release/exa
+
+build: CARGOFLAGS=
+build: all
+build-no-git: all
+
+target/release/exa: $(SRC)
+	if test -n "$$(echo "$$CC" | cut -d \  -f 1)"; then \
+	    env CC="$$(echo "$$CC" | cut -d \  -f 1)" cargo build --release $(CARGOFLAGS); \
+	else\
+	    env -u CC cargo build --release $(CARGOFLAGS); \
+	fi
+
+install: target/release/exa
 	# BSD and OSX don't have -D to create leading directories
-	install -dm755 $(PREFIX)/bin/ $(PREFIX)/share/man/man1/
-	install -sm755 target/release/exa $(PREFIX)/bin/
-	install -m644 contrib/man/*.1 $(PREFIX)/share/man/man1/
+	install -dm755 -- "$(DESTDIR)$(PREFIX)/bin/" "$(DESTDIR)$(PREFIX)/share/man/man1/"
+	install -m755 -- target/release/exa "$(DESTDIR)$(PREFIX)/bin/"
+	install -m644  -- contrib/man/exa.1 "$(DESTDIR)$(PREFIX)/share/man/man1/"
+
+uninstall:
+	-rm    -- "$(DESTDIR)$(PREFIX)/share/man/man1/exa.1"
+	-rmdir -- "$(DESTDIR)$(PREFIX)/share/man/man1"
+	-rm    -- "$(DESTDIR)$(PREFIX)/bin/exa"
+	-rmdir -- "$(DESTDIR)$(PREFIX)/bin"
 
-install: build $(INSTALL)
+clean:
+	-rm -rf target
 
-.PHONY: $(BUILD) build-no-git $(INSTALL)
+.PHONY: all build install

+ 27 - 27
README.md

@@ -14,21 +14,21 @@ exa’s options are similar, but not exactly the same, as `ls`.
 ### Display Options
 
 - **-1**, **--oneline**: display one entry per line
-- **-G**, **--grid**: display entries in a grid view (default)
+- **-G**, **--grid**: display entries as a grid (default)
 - **-l**, **--long**: display extended details and attributes
 - **-R**, **--recurse**: recurse into directories
-- **-T**, **--tree**: recurse into subdirectories in a tree view
-- **-x**, **--across**: sort multi-column view entries across
-- **--color**, **--colour**: when to colourise the output
-- **--color-scale**, **--colour-scale**: colour file sizes according to their magnitude
+- **-T**, **--tree**: recurse into directories as a tree
+- **-x**, **--across**: sort the grid across, rather than downwards
+- **--colo[u]r**: when to use terminal colours
+- **--colo[u]r-scale**: highlight levels of file sizes distinctly
 
 ### Filtering Options
 
-- **-a**, **--all**: show dot files
-- **-d**, **--list-dirs**: list directories as regular files
-- **-L**, **--level=(depth)**: maximum depth of recursion
-- **-r**, **--reverse**: reverse sort order
-- **-s**, **--sort=(field)**: field to sort by
+- **-a**, **--all**: don't hide hidden and 'dot' files
+- **-d**, **--list-dirs**: list directories like regular files
+- **-L**, **--level=(depth)**: limit the depth of recursion
+- **-r**, **--reverse**: reverse the sort order
+- **-s**, **--sort=(field)**: which field to sort by
 - **--group-directories-first**: list directories before other files
 - **-I**, **--ignore-glob=(globs)**: glob patterns (pipe-separated) of files to ignore
 
@@ -36,23 +36,23 @@ exa’s options are similar, but not exactly the same, as `ls`.
 
 These options are available when running with --long (`-l`):
 
-- **-b**, **--binary**: use binary (power of two) file sizes
-- **-B**, **--bytes**: list file sizes in bytes, without prefixes
-- **-g**, **--group**: show group as well as user
-- **-h**, **--header**: show a header row
-- **-H**, **--links**: show number of hard links column
-- **-i**, **--inode**: show inode number column
-- **-m**, **--modified**: display timestamp of most recent modification
-- **-S**, **--blocks**: show number of file system blocks
-- **-t**, **--time=(field)**: which timestamp to show for a file
-- **-u**, **--accessed**: display timestamp of last access for a file
-- **-U**, **--created**: display timestamp of creation of a file
-- **-@**, **--extended**: display extended attribute keys and sizes
-- **--git**: show Git status for a file
-
-Accepted **--color** options are **always**, **automatic**, and **never**.
-Valid sort fields are **name**, **size**, **extension**, **modified**, **accessed**, **created**, **inode**, and **none**.
-Valid time fields are **modified**, **accessed**, and **created**.
+- **-b**, **--binary**: list file sizes with binary prefixes
+- **-B**, **--bytes**: list file sizes in bytes, without any prefixes
+- **-g**, **--group**: list each file's group
+- **-h**, **--header**: add a header row to each column
+- **-H**, **--links**: list each file's number of hard links
+- **-i**, **--inode**: list each file's inode number
+- **-m**, **--modified**: use the modified timestamp field
+- **-S**, **--blocks**: list each file's number of file system blocks
+- **-t**, **--time=(field)**: which timestamp field to use
+- **-u**, **--accessed**: use the accessed timestamp field
+- **-U**, **--created**: use the created timestamp field
+- **-@**, **--extended**: list each file's extended attributes and sizes
+- **--git**: list each file's Git status, if tracked
+
+- Valid **--color** options are **always**, **automatic**, and **never**.
+- Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, and **none**. Fields starting with a capital letter are case-sensitive.
+- Valid time fields are **modified**, **accessed**, and **created**.
 
 
 ## Installation

+ 81 - 6
Vagrantfile

@@ -7,6 +7,8 @@ Vagrant.configure(2) do |config|
         v.cpus = 1
     end
 
+    developer = 'ubuntu'
+
 
     # We use Ubuntu instead of Debian because the image comes with two-way
     # shared folder support by default.
@@ -16,8 +18,11 @@ Vagrant.configure(2) do |config|
 
     # Install the dependencies needed for exa to build, as quietly as
     # apt can do.
-    config.vm.provision :shell, privileged: true, inline:
-        %[apt-get install -qq -o=Dpkg::Use-Pty=0 -y git cmake libssl-dev libgit2-dev libssh2-1-dev curl attr pkg-config]
+    config.vm.provision :shell, privileged: true, inline: <<-EOF
+        apt-get install -qq -o=Dpkg::Use-Pty=0 -y \
+          git cmake curl attr pkg-config libgit2-dev \
+          fish zsh bash bash-completion
+    EOF
 
 
     # Guarantee that the timezone is UTC -- some of the tests
@@ -38,13 +43,40 @@ Vagrant.configure(2) do |config|
     # By default it just uses the one in /vagrant/target, which can
     # cause problems if it has different permissions than the other
     # directories, or contains object files compiled for the host.
-    config.vm.provision :shell, privileged: false, inline:
-        %[echo "export CARGO_TARGET_DIR=/home/ubuntu/target" >> ~/.bashrc]
+    config.vm.provision :shell, privileged: false, inline: <<-EOF
+        function put_line() {
+          grep -q -F "$2" $1 || echo "$2" >> $1
+        }
+
+        put_line ~/.bashrc 'export CARGO_TARGET_DIR=/home/#{developer}/target'
+    EOF
+
+
+    # Create "dexa" and "rexa" scripts that run the debug and release
+    # compiled versions of exa.
+    config.vm.provision :shell, privileged: true, inline: <<-EOF
+        echo -e "#!/bin/sh\n/home/#{developer}/target/debug/exa \\$*" > /usr/bin/exa
+        echo -e "#!/bin/sh\n/home/#{developer}/target/release/exa \\$*" > /usr/bin/rexa
+        chmod +x /usr/bin/{exa,rexa}
+    EOF
+
+
+    # Link the completion files so they’re “installed”.
+    config.vm.provision :shell, privileged: true, inline: <<-EOF
+        test -h /etc/bash_completion.d/exa \
+          || ln -s /vagrant/contrib/completions.bash /etc/bash_completion.d/exa
+
+        test -h /usr/share/zsh/vendor-completions/_exa \
+          || ln -s /vagrant/contrib/completions.zsh /usr/share/zsh/vendor-completions/_exa
+
+        test -h /usr/share/fish/completions/exa.fish \
+          || ln -s /vagrant/contrib/completions.fish /usr/share/fish/completions/exa.fish
+    EOF
 
 
     # We create two users that own the test files.
-    # The first one just owns the ordinary ones, because we don’t want to
-    # depend on “vagrant” or “ubuntu” existing.
+    # The first one just owns the ordinary ones, because we don’t want the
+    # test outputs to depend on “vagrant” or “ubuntu” existing.
     user = "cassowary"
     config.vm.provision :shell, privileged: true, inline:
         %[id -u #{user} &>/dev/null || useradd #{user}]
@@ -142,6 +174,49 @@ Vagrant.configure(2) do |config|
     EOF
 
 
+    # File name testcases.
+    # bash really doesn’t want you to create a file with escaped characters
+    # in its name, so we have to resort to the echo builtin and touch!
+    #
+    # The double backslashes are not strictly necessary; without them, Ruby
+    # will interpolate them instead of bash, but because Vagrant prints out
+    # each command it runs, your *own* terminal will go “ding” from the alarm!
+    config.vm.provision :shell, privileged: false, inline: <<-EOF
+        set -xe
+        mkdir "#{test_dir}/file-names"
+
+        echo -ne "#{test_dir}/file-names/ascii: hello" | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/emoji: [🆒]"  | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/utf-8: pâté"  | xargs -0 touch
+
+        echo -ne "#{test_dir}/file-names/bell: [\\a]"         | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/backspace: [\\b]"    | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/form-feed: [\\f]"    | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/new-line: [\\n]"     | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/return: [\\r]"       | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/tab: [\\t]"          | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/vertical-tab: [\\v]" | xargs -0 touch
+
+        echo -ne "#{test_dir}/file-names/escape: [\\033]"               | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/ansi: [\\033[34mblue\\033[0m]" | xargs -0 touch
+
+        echo -ne "#{test_dir}/file-names/invalid-utf8-1: [\\xFF]"                | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/invalid-utf8-2: [\\xc3\\x28]"           | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/invalid-utf8-3: [\\xe2\\x82\\x28]"      | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/invalid-utf8-4: [\\xf0\\x28\\x8c\\x28]" | xargs -0 touch
+
+        echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]"                | xargs -0 mkdir
+        echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/subfile"        | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/another: [\\n]" | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken"         | xargs -0 touch
+
+        mkdir "#{test_dir}/file-names/links"
+        ln -s "#{test_dir}/file-names/new-line-dir"*/* "#{test_dir}/file-names/links"
+
+        echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken" | xargs -0 rm
+    EOF
+
+
     # Special file testcases.
     config.vm.provision :shell, privileged: false, inline: <<-EOF
         set -xe

+ 37 - 0
contrib/completions.bash

@@ -0,0 +1,37 @@
+_exa()
+{
+    cur=${COMP_WORDS[COMP_CWORD]}
+    prev=${COMP_WORDS[COMP_CWORD-1]}
+
+    case "$prev" in
+        -'?'|--help|-v|--version)
+            return
+            ;;
+
+        -L|--level)
+            COMPREPLY=( $( compgen -W '{0..9}' -- "$cur" ) )
+            return
+            ;;
+
+        -s|--sort)
+            COMPREPLY=( $( compgen -W 'name filename Name Filename size filesize extension Extension modified accessed created none inode --' -- "$cur" ) )
+            return
+            ;;
+
+        -t|--time)
+            COMPREPLY=( $( compgen -W 'accessed modified created --' -- $cur ) )
+            return
+            ;;
+    esac
+
+    case "$cur" in
+        -*)
+            COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) )
+            ;;
+
+        *)
+            _filedir
+            ;;
+    esac
+} &&
+complete -o filenames -o bashdefault -F _exa exa

+ 62 - 0
contrib/completions.fish

@@ -0,0 +1,62 @@
+# Meta-stuff
+complete -c exa -s 'v' -l 'version' -d "Show version of exa"
+complete -c exa -s '?' -l 'help'    -d "Show list of command-line options"
+
+# Display options
+complete -c exa -s '1' -l 'oneline'      -d "Display one entry per line"
+complete -c exa -s 'l' -l 'long'         -d "Display extended file metadata as a table"
+complete -c exa -s 'G' -l 'grid'         -d "Display entries in a grid"
+complete -c exa -s 'x' -l 'across'       -d "Sort the grid across, rather than downwards"
+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"
+
+# Filtering and sorting options
+complete -c exa -l 'group-directories-first' -d "Sort directories before other files"
+complete -c exa -s 'a' -l 'all'       -d "Don't hide 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 'r' -l 'reverse'   -d "Reverse the sort order"
+complete -c exa -s 's' -l 'sort'   -x -d "Which field to sort by" -a "
+    accessed\t'Sort by file accessed time'
+    created\t'Sort by file modified time'
+    ext\t'Sort by file extension'
+    Ext\t'Sort by file extension (case-insensitive)'
+    extension\t'Sort by file extension'
+    Extension\t'Sort by file extension (case-insensitive)'
+    filename\t'Sort by filename'
+    Filename\t'Sort by filename (case-insensitive)'
+    inode\t'Sort by file inode'
+    modified\t'Sort by file modified time'
+    name\t'Sort by filename'
+    Name\t'Sort by filename (case-insensitive)'
+    none\t'Do not sort files at all'
+    size\t'Sort by file size'
+"
+
+complete -c exa -s 'I' -l 'ignore-glob' -d "Ignore files that match these glob patterns" -r
+
+# Long view options
+complete -c exa -s 'b' -l 'binary'   -d "List file sizes with binary prefixes"
+complete -c exa -s 'B' -l 'bytes'    -d "List file sizes in bytes, without any prefixes"
+complete -c exa -s 'g' -l 'group'    -d "List each file's group"
+complete -c exa -s 'h' -l 'header'   -d "Add a header row to each column"
+complete -c exa -s 'h' -l 'links'    -d "List each file's number of hard links"
+complete -c exa -s 'g' -l 'group'    -d "List each file's inode number"
+complete -c exa -s 'm' -l 'modified' -d "Use the modified timestamp field"
+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 "
+    accessed\t'Display accessed time'
+    created\t'Display created time'
+    modified\t'Display modified time'
+"
+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"
+
+# Optional extras
+complete -c exa -s 'g' -l 'git'      -d "List each file's Git status, if tracked"
+complete -c exa -s '@' -l 'extended' -d "List each file's extended attributes and sizes"

+ 39 - 0
contrib/completions.zsh

@@ -0,0 +1,39 @@
+#compdef exa
+
+__exa() {
+    _arguments \
+        "(- 1 *)"{-v,--version}"[Show version of exa]" \
+        "(- 1 *)"{-\?,--help}"[Show list of command-line options]" \
+        {-1,--oneline}"[Display one entry per line]" \
+        {-l,--long}"[Display extended file metadata as a table]" \
+        {-G,--grid}"[Display entries as a grid]" \
+        {-x,--across}"[Sort the grid across, rather than downwards]" \
+        {-R,--recurse}"[Recurse into directories]" \
+        {-T,--tree}"[Recurse into directories as a tree]" \
+        {-F,--classify}"[Display type indicator by file names]" \
+        {--color,--colour}"[When to use terminal colours]" \
+        {--color,--colour}-scale"[Highlight levels of file sizes distinctly]" \
+        --group-directories-first"[Sort directories before other files]" \
+        {-a,--all}"[Don't hide hidden and 'dot' files]" \
+        {-d,--list-dirs}"[List directories like regular files]" \
+        {-L,--level}"+[Limit the depth of recursion]" \
+        {-r,--reverse}"[Reverse the sort order]" \
+        {-s,--sort}"[Which field to sort by]:(sort field):(accessed created extension Extension filename Filename inode modified name Name none size)" \
+        {-I,--ignore-glob}"[Ignore files that match these glob patterns]" \
+        {-b,--binary}"[List file sizes with binary prefixes]" \
+        {-B,--bytes}"[List file sizes in bytes, without any prefixes]" \
+        {-g,--group}"[List each file's group]" \
+        {-h,--header}"[Add a header row to each column]" \
+        {-H,--links}"[List each file's number of hard links]" \
+        {-i,--inode}"[List each file's inode number]" \
+        {-m,--modified}"[Use the modified timestamp field]" \
+        {-S,--blocks}"[List each file's number of filesystem blocks]" \
+        {-t,--time}"[Which time field to show]:(time field):(accessed created modified)" \
+        {-u,--accessed}"[Use the accessed timestamp field]" \
+        {-U,--created}"[Use the created timestamp field]" \
+        --git"[List each file's Git status, if tracked]" \
+        {-@,--extended}"[List each file's extended attributes and sizes]" \
+        '*:filename:_files'
+}
+
+__exa

+ 39 - 28
contrib/man/exa.1

@@ -1,5 +1,5 @@
 .hy
-.TH "exa" "1" "2015\-10\-18" "exa 0.4.0" ""
+.TH "exa" "1" "2017\-05\-06" "exa 0.5.0" ""
 .SH NAME
 .PP
 exa \- a modern replacement for ls
@@ -23,12 +23,17 @@ display one entry per line
 .RE
 .TP
 .B \-G, \-\-grid
-display entries in a grid view (default)
+display entries as a grid (default)
 .RS
 .RE
 .TP
 .B \-l, \-\-long
-display extended details and attributes
+display extended file metadata as a table
+.RS
+.RE
+.TP
+.B \-x, \-\-across
+sort the grid across, rather than downwards
 .RS
 .RE
 .TP
@@ -38,39 +43,45 @@ recurse into directories
 .RE
 .TP
 .B \-T, \-\-tree
-recurse into subdirectories in a tree view
+recurse into directories as a tree
 .RS
 .RE
 .TP
-.B \-x, \-\-across
-sort multi\-column view entries across
+.B \-\-color, \-\-colour=\f[I]WHEN\f[]
+when to use terminal colours (always, automatic, never)
 .RS
 .RE
 .TP
-.B \-\-color, \-\-colour=\f[I]WHEN\f[]
-when to colourise the output (always, automatic, never)
+.B \-\-color-scale, \-\-colour-scale
+highlight levels of file sizes distinctly
 .RS
 .RE
 .SH FILTERING AND SORTING OPTIONS
 .TP
 .B \-a, \-\-all
-show dot\-files
+don\[aq]t hide hidden and \[aq]dot\[aq] files
 .RS
 .RE
 .TP
 .B \-d, \-\-list\-dirs
-list directories as regular files
+list directories like regular files
 .RS
 .RE
 .TP
 .B \-r, \-\-reverse
-reverse order of files
+reverse the sort order
+.RS
+.RE
+.TP
+.B \-s, \-\-sort=\f[I]SORT_FIELD\f[]
+which field to sort by.
+Valid fields are name, Name, extension, Extension, size, modified, accessed, created, inode, and none.
+Fields starting with a capital letter are case-sensitive.
 .RS
 .RE
 .TP
-.B \-s, \-\-sort=\f[I]WORD\f[]
-field to sort by (name, size, extension, modified, accessed, created,
-inode, none)
+.B \-I, \-\-ignore\-glob=\f[I]GLOBS\f[]
+Glob patterns, pipe-separated, of files to ignore
 .RS
 .RE
 .TP
@@ -84,72 +95,72 @@ These options are available when running with \f[C]\-\-long\f[]
 (\f[C]\-l\f[]):
 .TP
 .B \-b, \-\-binary
-use binary prefixes in file sizes
+list file sizes with binary prefixes
 .RS
 .RE
 .TP
 .B \-B, \-\-bytes
-list file sizes in bytes, without prefixes
+list file sizes in bytes, without any prefixes
 .RS
 .RE
 .TP
 .B \-g, \-\-group
-show group as well as user
+list each file\[aq]s group
 .RS
 .RE
 .TP
 .B \-h, \-\-header
-show a header row at the top
+add a header row to each column
 .RS
 .RE
 .TP
 .B \-H, \-\-links
-show number of hard links
+list each file\[aq]s number of hard links
 .RS
 .RE
 .TP
 .B \-i, \-\-inode
-show each file\[aq]s inode number
+list each file\[aq]s inode number
 .RS
 .RE
 .TP
 .B \-L, \-\-level=\f[I]DEPTH\f[]
-maximum depth of recursion
+limit the depth of recursion
 .RS
 .RE
 .TP
 .B \-m, \-\-modified
-display timestamp of most recent modification
+use the modified timestamp field
 .RS
 .RE
 .TP
 .B \-S, \-\-blocks
-show number of file system blocks
+list each file\[aq]s number of file system blocks
 .RS
 .RE
 .TP
 .B \-t, \-\-time=\f[I]WORD\f[]
-which timestamp to show for a file (modified, accessed, created)
+which timestamp field to list (modified, accessed, created)
 .RS
 .RE
 .TP
 .B \-u, \-\-accessed
-display timestamp of last access for a file
+use the accessed timestamp field
 .RS
 .RE
 .TP
 .B \-U, \-\-created
-display timestamp of creation for a file
+use the created timestamp field
 .RS
 .RE
 .TP
 .B \-\@, \-\-extended
-display extended attribute keys and sizes
+list each file\[aq]s extended attributes and sizes
 .RS
 .RE
 .TP
 .B \-\-git
-display Git status for a file, if available
+list each file\[aq]s Git status, if tracked
 .RS
 .RE
 .SH EXAMPLES

+ 6 - 1
src/exa.rs

@@ -23,9 +23,12 @@ use std::ffi::OsStr;
 use std::io::{stderr, Write, Result as IOResult};
 use std::path::{Component, Path};
 
+use ansi_term::{ANSIStrings, Style};
+
 use fs::{Dir, File};
 use options::{Options, View};
 pub use options::Misfire;
+use output::escape;
 
 mod fs;
 mod info;
@@ -116,7 +119,9 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
             }
 
             if !is_only_dir {
-                writeln!(self.writer, "{}:", dir.path.display())?;
+                let mut bits = Vec::new();
+                escape(dir.path.display().to_string(), &mut bits, Style::default(), Style::default());
+                writeln!(self.writer, "{}:", ANSIStrings(&bits))?;
             }
 
             let mut children = Vec::new();

+ 56 - 77
src/fs/file.rs

@@ -1,45 +1,32 @@
 //! Files, and methods and fields to access their metadata.
 
-use std::ascii::AsciiExt;
-use std::env::current_dir;
 use std::fs;
 use std::io::Error as IOError;
 use std::io::Result as IOResult;
-use std::os::unix::fs::{MetadataExt, PermissionsExt};
+use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt};
 use std::path::{Path, PathBuf};
 
 use fs::dir::Dir;
 use fs::fields as f;
 
-#[cfg(any(target_os = "macos", target_os = "linux"))]
-use std::os::unix::fs::FileTypeExt;
 
-/// Constant table copied from https://doc.rust-lang.org/src/std/sys/unix/ext/fs.rs.html#11-259
-/// which is currently unstable and lacks vision for stabilization,
-/// see https://github.com/rust-lang/rust/issues/27712
-#[allow(dead_code, non_camel_case_types)]
+#[allow(trivial_numeric_casts)]
 mod modes {
-    pub type mode_t = u32;
-
-    pub const USER_READ: mode_t = 0o400;
-    pub const USER_WRITE: mode_t = 0o200;
-    pub const USER_EXECUTE: mode_t = 0o100;
-    pub const USER_RWX: mode_t = 0o700;
-    pub const GROUP_READ: mode_t = 0o040;
-    pub const GROUP_WRITE: mode_t = 0o020;
-    pub const GROUP_EXECUTE: mode_t = 0o010;
-    pub const GROUP_RWX: mode_t = 0o070;
-    pub const OTHER_READ: mode_t = 0o004;
-    pub const OTHER_WRITE: mode_t = 0o002;
-    pub const OTHER_EXECUTE: mode_t = 0o001;
-    pub const OTHER_RWX: mode_t = 0o007;
-    pub const ALL_READ: mode_t = 0o444;
-    pub const ALL_WRITE: mode_t = 0o222;
-    pub const ALL_EXECUTE: mode_t = 0o111;
-    pub const ALL_RWX: mode_t = 0o777;
-    pub const SETUID: mode_t = 0o4000;
-    pub const SETGID: mode_t = 0o2000;
-    pub const STICKY_BIT: mode_t = 0o1000;
+    use libc;
+
+    pub type Mode = u32;
+    // The `libc::mode_t` type’s actual type varies, but the value returned
+    // from `metadata.permissions().mode()` is always `u32`.
+
+    pub const USER_READ: Mode     = libc::S_IRUSR as Mode;
+    pub const USER_WRITE: Mode    = libc::S_IWUSR as Mode;
+    pub const USER_EXECUTE: Mode  = libc::S_IXUSR as Mode;
+    pub const GROUP_READ: Mode    = libc::S_IRGRP as Mode;
+    pub const GROUP_WRITE: Mode   = libc::S_IWGRP as Mode;
+    pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode;
+    pub const OTHER_READ: Mode    = libc::S_IROTH as Mode;
+    pub const OTHER_WRITE: Mode   = libc::S_IWOTH as Mode;
+    pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode;
 }
 
 
@@ -150,6 +137,27 @@ impl<'dir> File<'dir> {
         self.metadata.file_type().is_symlink()
     }
 
+    /// Whether this file is a named pipe on the filesystem.
+    pub fn is_pipe(&self) -> bool {
+       self.metadata.file_type().is_fifo()
+   }
+
+   /// Whether this file is a char device on the filesystem.
+   pub fn is_char_device(&self) -> bool {
+       self.metadata.file_type().is_char_device()
+   }
+
+   /// Whether this file is a block device on the filesystem.
+   pub fn is_block_device(&self) -> bool {
+       self.metadata.file_type().is_block_device()
+   }
+
+   /// Whether this file is a socket on the filesystem.
+   pub fn is_socket(&self) -> bool {
+       self.metadata.file_type().is_socket()
+   }
+
+
     /// Whether this file is a dotfile, based on its name. In Unix, file names
     /// beginning with a dot represent system or configuration files, and
     /// should be hidden by default.
@@ -344,6 +352,8 @@ impl<'dir> File<'dir> {
     /// directory, so will not work if this file has just been passed in on
     /// the command line.
     pub fn git_status(&self) -> f::Git {
+        use std::env::current_dir;
+
         match self.dir {
             None    => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
             Some(d) => {
@@ -358,52 +368,6 @@ impl<'dir> File<'dir> {
     }
 }
 
-#[cfg(any(target_os = "macos", target_os = "linux"))]
-impl<'dir> File<'dir> {
-    /// Whether this file is a named pipe on the filesystem.
-    pub fn is_pipe(&self) -> bool {
-        self.metadata.file_type().is_fifo()
-    }
-
-    /// Whether this file is a char device on the filesystem.
-    pub fn is_char_device(&self) -> bool {
-        self.metadata.file_type().is_char_device()
-    }
-
-    /// Whether this file is a block device on the filesystem.
-    pub fn is_block_device(&self) -> bool {
-        self.metadata.file_type().is_block_device()
-    }
-
-    /// Whether this file is a socket on the filesystem.
-    pub fn is_socket(&self) -> bool {
-        self.metadata.file_type().is_socket()
-    }
-}
-
-#[cfg(not(any(target_os = "macos", target_os = "linux")))]
-impl<'dir> File<'dir> {
-    /// Whether this file is a named pipe on the filesystem.
-    pub fn is_pipe(&self) -> bool {
-        false
-    }
-
-    /// Whether this file is a char device on the filesystem.
-    pub fn is_char_device(&self) -> bool {
-        false
-    }
-
-    /// Whether this file is a block device on the filesystem.
-    pub fn is_block_device(&self) -> bool {
-        false
-    }
-
-    /// Whether this file is a socket on the filesystem.
-    pub fn is_socket(&self) -> bool {
-        false
-    }
-}
-
 
 impl<'a> AsRef<File<'a>> for File<'a> {
     fn as_ref(&self) -> &File<'a> {
@@ -421,6 +385,8 @@ impl<'a> AsRef<File<'a>> for File<'a> {
 /// against a pre-compiled list of extensions which are known to only exist
 /// within ASCII, so it's alright.
 fn ext(path: &Path) -> Option<String> {
+    use std::ascii::AsciiExt;
+
     let name = match path.file_name() {
         Some(f) => f.to_string_lossy().to_string(),
         None => return None,
@@ -441,11 +407,24 @@ pub enum FileTarget<'dir> {
     Broken(PathBuf),
 
     /// There was an IO error when following the link. This can happen if the
-    /// file isn't a link to begin with, but also if, say, we don't have
+    /// file isn’t a link to begin with, but also if, say, we don’t have
     /// permission to follow it.
     Err(IOError),
 }
 
+impl<'dir> FileTarget<'dir> {
+
+    /// Whether this link doesn’t lead to a file, for whatever reason. This
+    /// gets used to determine how to highlight the link in grid views.
+    pub fn is_broken(&self) -> bool {
+        match self {
+            &FileTarget::Ok(_)      => false,
+            &FileTarget::Broken(_)  => true,
+            &FileTarget::Err(_)     => true,
+        }
+    }
+}
+
 
 #[cfg(test)]
 mod test {

+ 30 - 28
src/options/help.rs

@@ -1,44 +1,46 @@
 
 pub static OPTIONS: &'static str = r##"
+  -?, --help         show list of command-line options
+  -v, --version      show version of exa
+
 DISPLAY OPTIONS
   -1, --oneline      display one entry per line
-  -G, --grid         display entries in a grid view (default)
-  -l, --long         display extended details and attributes
+  -l, --long         display extended file metadata as a table
+  -G, --grid         display entries as a grid (default)
+  -x, --across       sort the grid across, rather than downwards
   -R, --recurse      recurse into directories
-  -T, --tree         recurse into subdirectories in a tree view
-  -x, --across       sort multi-column view entries across
-  -F, --classify     show file type indicator (one of */=@|)
-
-  --color=WHEN,  --colour=WHEN   when to colourise the output (always, auto, never)
-  --color-scale, --colour-scale  colour file sizes according to their magnitude
+  -T, --tree         recurse into directories as a tree
+  -F, --classify     display type indicator by file names
+  --colo[u]r=WHEN    when to use terminal colours (always, auto, never)
+  --colo[u]r-scale   highlight levels of file sizes distinctly
 
 FILTERING AND SORTING OPTIONS
-  -a, --all                  show dot-files
-  -d, --list-dirs            list directories as regular files
-  -r, --reverse              reverse order of files
-  -s, --sort SORT_FIELD      field to sort by. Choices: name,
-                                 size, extension, modified,
-                                 accessed, created, inode, none
+  -a, --all                  don't hide hidden and 'dot' files
+  -d, --list-dirs            list directories like regular files
+  -r, --reverse              reverse the sort order
+  -s, --sort SORT_FIELD      which field to sort by:
   --group-directories-first  list directories before other files
   -I, --ignore-glob GLOBS    glob patterns (pipe-separated) of files to ignore
+  Valid sort fields:         name, Name, extension, Extension, size,
+                             modified, accessed, created, inode, none
+
 "##;
 
 pub static LONG_OPTIONS: &'static str = r##"
 LONG VIEW OPTIONS
-  -b, --binary       use binary prefixes in file sizes
-  -B, --bytes        list file sizes in bytes, without prefixes
-  -g, --group        show group as well as user
-  -h, --header       show a header row at the top
-  -H, --links        show number of hard links
-  -i, --inode        show each file's inode number
-  -L, --level DEPTH  maximum depth of recursion
-  -m, --modified     display timestamp of most recent modification
+  -b, --binary       list file sizes with binary prefixes
+  -B, --bytes        list file sizes in bytes, without any prefixes
+  -g, --group        list each file's group
+  -h, --header       add a header row to each column
+  -H, --links        list each file's number of hard links
+  -i, --inode        list each file's inode number
+  -L, --level DEPTH  limit the depth of recursion
+  -m, --modified     use the modified timestamp field
   -S, --blocks       show number of file system blocks
-  -t, --time FIELD   which timestamp to show for a file. Choices:
-                         modified, accessed, created
-  -u, --accessed     display timestamp of last access for a file
-  -U, --created      display timestamp of creation for a file
+  -t, --time FIELD   which timestamp field to list (modified, accessed, created)
+  -u, --accessed     use the accessed timestamp field
+  -U, --created      use the created timestamp field
 "##;
 
-pub static GIT_HELP:      &'static str = r##"  --git              show git status for files"##;
-pub static EXTENDED_HELP: &'static str = r##"  -@, --extended     display extended attribute keys and sizes"##;
+pub static GIT_HELP:      &'static str = r##"  --git              list each file's Git status, if tracked"##;
+pub static EXTENDED_HELP: &'static str = r##"  -@, --extended     list each file's extended attributes and sizes"##;

+ 29 - 29
src/options/mod.rs

@@ -45,50 +45,50 @@ impl Options {
     where S: AsRef<OsStr> {
         let mut opts = getopts::Options::new();
 
-        opts.optflag("v", "version",   "display version of exa");
+        opts.optflag("v", "version",   "show version of exa");
         opts.optflag("?", "help",      "show list of command-line options");
 
         // Display options
         opts.optflag("1", "oneline",      "display one entry per line");
-        opts.optflag("G", "grid",         "display entries in a grid view (default)");
-        opts.optflag("l", "long",         "display extended details and attributes");
+        opts.optflag("l", "long",         "display extended file metadata in a table");
+        opts.optflag("G", "grid",         "display entries as a grid (default)");
+        opts.optflag("x", "across",       "sort the grid across, rather than downwards");
         opts.optflag("R", "recurse",      "recurse into directories");
-        opts.optflag("T", "tree",         "recurse into subdirectories in a tree view");
-        opts.optflag("x", "across",       "sort multi-column view entries across");
-        opts.optflag("F", "classify",     "show file type indicator (one of */=@|)");
-        opts.optopt ("",  "color",        "when to show anything in colours", "WHEN");
-        opts.optopt ("",  "colour",       "when to show anything in colours (alternate spelling)", "WHEN");
-        opts.optflag("",  "color-scale",  "use a colour scale when displaying file sizes (alternate spelling)");
-        opts.optflag("",  "colour-scale", "use a colour scale when displaying file sizes");
+        opts.optflag("T", "tree",         "recurse into directories as a tree");
+        opts.optflag("F", "classify",     "display type indicator by file names (one of */=@|)");
+        opts.optopt ("",  "color",        "when to use terminal colours", "WHEN");
+        opts.optopt ("",  "colour",       "when to use terminal colours", "WHEN");
+        opts.optflag("",  "color-scale",  "highlight levels of file sizes distinctly");
+        opts.optflag("",  "colour-scale", "highlight levels of file sizes distinctly");
 
         // Filtering and sorting options
-        opts.optflag("",  "group-directories-first", "list directories before other files");
-        opts.optflag("a", "all",         "show dot-files");
-        opts.optflag("d", "list-dirs",   "list directories as regular files");
-        opts.optflag("r", "reverse",     "reverse order of files");
-        opts.optopt ("s", "sort",        "field to sort by", "WORD");
-        opts.optopt ("I", "ignore-glob", "patterns (|-separated) of names to ignore", "GLOBS");
+        opts.optflag("",  "group-directories-first", "sort directories before other files");
+        opts.optflag("a", "all",         "don't hide hidden and 'dot' files");
+        opts.optflag("d", "list-dirs",   "list directories like regular files");
+        opts.optopt ("L", "level",       "limit the depth of recursion", "DEPTH");
+        opts.optflag("r", "reverse",     "reverse the sert order");
+        opts.optopt ("s", "sort",        "which field to sort by", "WORD");
+        opts.optopt ("I", "ignore-glob", "ignore files that match these glob patterns", "GLOB1|GLOB2...");
 
         // Long view options
-        opts.optflag("b", "binary",    "use binary prefixes in file sizes");
+        opts.optflag("b", "binary",    "list file sizes with binary prefixes");
         opts.optflag("B", "bytes",     "list file sizes in bytes, without prefixes");
-        opts.optflag("g", "group",     "show group as well as user");
-        opts.optflag("h", "header",    "show a header row at the top");
-        opts.optflag("H", "links",     "show number of hard links");
-        opts.optflag("i", "inode",     "show each file's inode number");
-        opts.optopt ("L", "level",     "maximum depth of recursion", "DEPTH");
-        opts.optflag("m", "modified",  "display timestamp of most recent modification");
-        opts.optflag("S", "blocks",    "show number of file system blocks");
-        opts.optopt ("t", "time",      "which timestamp to show for a file", "WORD");
-        opts.optflag("u", "accessed",  "display timestamp of last access for a file");
-        opts.optflag("U", "created",   "display timestamp of creation for a file");
+        opts.optflag("g", "group",     "list each file's group");
+        opts.optflag("h", "header",    "add a header row to each column");
+        opts.optflag("H", "links",     "list each file's number of hard links");
+        opts.optflag("i", "inode",     "list each file's inode number");
+        opts.optflag("m", "modified",  "use the modified timestamp field");
+        opts.optflag("S", "blocks",    "list each file's number of file system blocks");
+        opts.optopt ("t", "time",      "which timestamp field to show", "WORD");
+        opts.optflag("u", "accessed",  "use the accessed timestamp field");
+        opts.optflag("U", "created",   "use the created timestamp field");
 
         if cfg!(feature="git") {
-            opts.optflag("", "git", "show git status");
+            opts.optflag("", "git", "list each file's git status");
         }
 
         if xattr::ENABLED {
-            opts.optflag("@", "extended", "display extended attribute keys and sizes");
+            opts.optflag("@", "extended", "list each file's extended attribute keys and sizes");
         }
 
         let matches = match opts.parse(args) {

+ 13 - 4
src/options/view.rs

@@ -4,8 +4,9 @@ use getopts;
 
 use output::Colours;
 use output::{Grid, Details, GridDetails, Lines};
-use options::{FileFilter, DirAction, Misfire};
 use output::column::{Columns, TimeTypes, SizeFormat};
+use output::file_name::Classify;
+use options::{FileFilter, DirAction, Misfire};
 use term::dimensions;
 use fs::feature::xattr;
 
@@ -58,7 +59,7 @@ impl View {
                     filter: filter.clone(),
                     xattr: xattr::ENABLED && matches.opt_present("extended"),
                     colours: colours,
-                    classify: matches.opt_present("classify"),
+                    classify: Classify::deduce(matches),
                 };
 
                 Ok(details)
@@ -87,8 +88,7 @@ impl View {
         };
 
         let other_options_scan = || {
-            let classify = matches.opt_present("classify");
-
+            let classify     = Classify::deduce(matches);
             let term_colours = TerminalColours::deduce(matches)?;
             let term_width   = TerminalWidth::deduce()?;
 
@@ -366,3 +366,12 @@ impl TerminalColours {
         }
     }
 }
+
+
+
+impl Classify {
+    fn deduce(matches: &getopts::Matches) -> Classify {
+        if matches.opt_present("classify") { Classify::AddFileIndicators }
+                                      else { Classify::JustFilenames }
+    }
+}

+ 16 - 15
src/output/cell.rs

@@ -5,8 +5,6 @@ use std::ops::{Add, Deref, DerefMut};
 use ansi_term::{Style, ANSIString, ANSIStrings};
 use unicode_width::UnicodeWidthStr;
 
-use fs::File;
-
 
 /// An individual cell that holds text in a table, used in the details and
 /// lines views to store ANSI-terminal-formatted data before it is printed.
@@ -161,6 +159,22 @@ impl TextCellContents {
     pub fn strings(&self) -> ANSIStrings {
         ANSIStrings(&self.0)
     }
+
+    /// Calculates the width that a cell with these contents would take up, by
+    /// counting the number of characters in each unformatted ANSI string.
+    pub fn width(&self) -> DisplayWidth {
+        let foo = self.0.iter().map(|anstr| anstr.chars().count()).sum();
+        DisplayWidth(foo)
+    }
+
+    /// Promotes these contents to a full cell containing them alongside
+    /// their calculated width.
+    pub fn promote(self) -> TextCell {
+        TextCell {
+            width: self.width(),
+            contents: self,
+        }
+    }
 }
 
 
@@ -180,19 +194,6 @@ impl TextCellContents {
 #[derive(PartialEq, Debug, Clone, Copy, Default)]
 pub struct DisplayWidth(usize);
 
-impl DisplayWidth {
-    pub fn from_file(file: &File, classify: bool) -> DisplayWidth {
-        let name_width = *DisplayWidth::from(&*file.name);
-        if classify {
-            if file.is_executable_file() || file.is_directory() ||
-                file.is_pipe() || file.is_link() || file.is_socket() {
-                return DisplayWidth(name_width + 1);
-            }
-        }
-        DisplayWidth(name_width)
-    }
-}
-
 impl<'a> From<&'a str> for DisplayWidth {
     fn from(input: &'a str) -> DisplayWidth {
         DisplayWidth(UnicodeWidthStr::width(input))

+ 3 - 1
src/output/colours.rs

@@ -22,6 +22,7 @@ pub struct Colours {
     pub symlink_path:     Style,
     pub broken_arrow:     Style,
     pub broken_filename:  Style,
+    pub control_char:     Style,
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
@@ -170,7 +171,8 @@ impl Colours {
 
             symlink_path:     Cyan.normal(),
             broken_arrow:     Red.normal(),
-            broken_filename:  Red.underline()
+            broken_filename:  Red.underline(),
+            control_char:     Red.normal(),
         }
     }
 

+ 6 - 29
src/output/details.rs

@@ -99,9 +99,9 @@ use fs::feature::xattr::{Attribute, FileAttributes};
 use options::{FileFilter, RecurseOptions};
 use output::colours::Colours;
 use output::column::{Alignment, Column, Columns, SizeFormat};
-use output::cell::{TextCell, DisplayWidth};
+use output::cell::{TextCell, TextCellContents, DisplayWidth};
 use output::tree::TreeTrunk;
-use super::filename;
+use output::file_name::{FileName, LinkStyle, Classify};
 
 
 /// With the **Details** view, the output gets formatted into columns, with
@@ -142,7 +142,7 @@ pub struct Details {
     pub colours: Colours,
 
     /// Whether to show a file type indiccator.
-    pub classify: bool,
+    pub classify: Classify,
 }
 
 /// The **environment** struct contains any data that could change between
@@ -306,23 +306,11 @@ impl Details {
         for (index, egg) in file_eggs.into_iter().enumerate() {
             let mut files = Vec::new();
             let mut errors = egg.errors;
-            let mut width = DisplayWidth::from_file(&egg.file, self.classify);
-
-            if egg.file.dir.is_none() {
-                if let Some(parent) = egg.file.path.parent() {
-                    width = width + 1 + DisplayWidth::from(parent.to_string_lossy().as_ref());
-                }
-            }
-
-            let name = TextCell {
-                contents: filename(&egg.file, &self.colours, true, self.classify),
-                width:    width,
-            };
 
             let row = Row {
                 depth:    depth,
                 cells:    Some(egg.cells),
-                name:     name,
+                name:     FileName::new(&egg.file, LinkStyle::FullLinkPaths, self.classify, &self.colours).paint().promote(),
                 last:     index == num_eggs - 1,
             };
 
@@ -455,19 +443,8 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
         self.rows.push(row);
     }
 
-    pub fn filename_cell(&self, file: File, links: bool) -> TextCell {
-        let mut width = DisplayWidth::from_file(&file, self.opts.classify);
-
-        if file.dir.is_none() {
-            if let Some(parent) = file.path.parent() {
-                width = width + 1 + DisplayWidth::from(parent.to_string_lossy().as_ref());
-            }
-        }
-
-        TextCell {
-            contents: filename(&file, &self.opts.colours, links, self.opts.classify),
-            width:    width,
-        }
+    pub fn filename(&self, file: File, links: LinkStyle) -> TextCellContents {
+        FileName::new(&file, links, self.opts.classify, &self.opts.colours).paint()
     }
 
     pub fn add_file_with_cells(&mut self, cells: Vec<TextCell>, name_cell: TextCell, depth: usize, last: bool) {

+ 25 - 0
src/output/escape.rs

@@ -0,0 +1,25 @@
+use ansi_term::{ANSIString, Style};
+
+
+pub fn escape<'a>(string: String, bits: &mut Vec<ANSIString<'a>>, good: Style, bad: Style) {
+    if string.chars().all(|c| c >= 0x20 as char) {
+        bits.push(good.paint(string));
+    }
+    else {
+        for c in string.chars() {
+            // The `escape_default` method on `char` is *almost* what we want here, but
+            // it still escapes non-ASCII UTF-8 characters, which are still printable.
+
+            if c >= 0x20 as char {
+                // TODO: This allocates way too much,
+                // hence the `all` check above.
+                let mut s = String::new();
+                s.push(c);
+                bits.push(good.paint(s));
+            } else {
+                let s = c.escape_default().collect::<String>();
+                bits.push(bad.paint(s));
+            }
+        }
+    }
+}

+ 238 - 0
src/output/file_name.rs

@@ -0,0 +1,238 @@
+use std::path::Path;
+
+use ansi_term::{ANSIString, Style};
+
+use fs::{File, FileTarget};
+use output::Colours;
+use output::escape;
+use output::cell::TextCellContents;
+
+
+/// A **file name** holds all the information necessary to display the name
+/// of the given file. This is used in all of the views.
+pub struct FileName<'a, 'dir: 'a> {
+
+    /// A reference to the file that we're getting the name of.
+    file: &'a File<'dir>,
+
+    /// The colours used to paint the file name and its surrounding text.
+    colours: &'a Colours,
+
+    /// The file that this file points to if it's a link.
+    target: Option<FileTarget<'dir>>,
+
+    /// How to handle displaying links.
+    link_style: LinkStyle,
+
+    /// Whether to append file class characters to file names.
+    classify: Classify,
+}
+
+
+impl<'a, 'dir> FileName<'a, 'dir> {
+
+    /// Create a new `FileName` that prints the given file’s name, painting it
+    /// with the remaining arguments.
+    pub fn new(file: &'a File<'dir>, link_style: LinkStyle, classify: Classify, colours: &'a Colours) -> FileName<'a, 'dir> {
+        let target =  if file.is_link() { Some(file.link_target()) }
+                                                       else { None };
+        FileName {
+            file: file,
+            colours: colours,
+            target: target,
+            link_style: link_style,
+            classify: classify,
+        }
+    }
+
+
+    /// Paints the name of the file using the colours, resulting in a vector
+    /// of coloured cells that can be printed to the terminal.
+    ///
+    /// This method returns some `TextCellContents`, rather than a `TextCell`,
+    /// because for the last cell in a table, it doesn’t need to have its
+    /// width calculated.
+    pub fn paint(&self) -> TextCellContents {
+        let mut bits = Vec::new();
+
+        if self.file.dir.is_none() {
+            if let Some(parent) = self.file.path.parent() {
+                self.add_parent_bits(&mut bits, parent);
+            }
+        }
+
+        if !self.file.name.is_empty() {
+            for bit in self.coloured_file_name() {
+                bits.push(bit);
+            }
+        }
+
+        if let (LinkStyle::FullLinkPaths, Some(ref target)) = (self.link_style, self.target.as_ref()) {
+            match **target {
+                FileTarget::Ok(ref target) => {
+                    bits.push(Style::default().paint(" "));
+                    bits.push(self.colours.punctuation.paint("->"));
+                    bits.push(Style::default().paint(" "));
+
+                    if let Some(parent) = target.path.parent() {
+                        self.add_parent_bits(&mut bits, parent);
+                    }
+
+                    if !target.name.is_empty() {
+                        let target = FileName::new(&target, LinkStyle::FullLinkPaths, Classify::JustFilenames, self.colours);
+                        for bit in target.coloured_file_name() {
+                            bits.push(bit);
+                        }
+                    }
+                },
+
+                FileTarget::Broken(ref broken_path) => {
+                    bits.push(Style::default().paint(" "));
+                    bits.push(self.colours.broken_arrow.paint("->"));
+                    bits.push(Style::default().paint(" "));
+                    escape(broken_path.display().to_string(), &mut bits, self.colours.broken_filename, self.colours.control_char.underline());
+                },
+
+                FileTarget::Err(_) => {
+                    // Do nothing -- the error gets displayed on the next line
+                },
+            }
+        }
+        else if let Classify::AddFileIndicators = self.classify {
+            if let Some(class) = self.classify_char() {
+                bits.push(Style::default().paint(class));
+            }
+        }
+
+        bits.into()
+    }
+
+
+    /// Adds the bits of the parent path to the given bits vector.
+    /// The path gets its characters escaped based on the colours.
+    fn add_parent_bits(&self, bits: &mut Vec<ANSIString>, parent: &Path) {
+        let coconut = parent.components().count();
+
+        if coconut == 1 && parent.has_root() {
+            bits.push(self.colours.symlink_path.paint("/"));
+        }
+        else if coconut >= 1 {
+            escape(parent.to_string_lossy().to_string(), bits, self.colours.symlink_path, self.colours.control_char);
+            bits.push(self.colours.symlink_path.paint("/"));
+        }
+    }
+
+
+    /// The character to be displayed after a file when classifying is on, if
+    /// the file’s type has one associated with it.
+    fn classify_char(&self) -> Option<&'static str> {
+        if self.file.is_executable_file() {
+            Some("*")
+        } else if self.file.is_directory() {
+            Some("/")
+        } else if self.file.is_pipe() {
+            Some("|")
+        } else if self.file.is_link() {
+            Some("@")
+        } else if self.file.is_socket() {
+            Some("=")
+        } else {
+            None
+        }
+    }
+
+
+    /// Returns at least one ANSI-highlighted string representing this file’s
+    /// name using the given set of colours.
+    ///
+    /// Ordinarily, this will be just one string: the file’s complete name,
+    /// coloured according to its file type. If the name contains control
+    /// characters such as newlines or escapes, though, we can’t just print them
+    /// to the screen directly, because then there’ll be newlines in weird places.
+    ///
+    /// So in that situation, those characters will be escaped and highlighted in
+    /// a different colour.
+    fn coloured_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
+        let file_style = self.style();
+        let mut bits = Vec::new();
+        escape(self.file.name.clone(), &mut bits, file_style, self.colours.control_char);
+        bits
+    }
+
+
+    /// Figures out which colour to paint the filename part of the output,
+    /// depending on which “type” of file it appears to be -- either from the
+    /// class on the filesystem or from its name.
+    pub fn style(&self) -> Style {
+
+        // Override the style with the “broken link” style when this file is
+        // a link that we can’t follow for whatever reason. This is used when
+        // there’s no other place to show that the link doesn’t work.
+        if let LinkStyle::JustFilenames = self.link_style {
+            if let Some(ref target) = self.target {
+                if target.is_broken() {
+                    return self.colours.broken_arrow;
+                }
+            }
+        }
+
+        // Otherwise, just apply a bunch of rules in order. For example,
+        // executable image files should be executable rather than images.
+        match self.file {
+            f if f.is_directory()        => self.colours.filetypes.directory,
+            f if f.is_executable_file()  => self.colours.filetypes.executable,
+            f if f.is_link()             => self.colours.filetypes.symlink,
+            f if f.is_pipe()             => self.colours.filetypes.pipe,
+            f if f.is_char_device()
+               | f.is_block_device()     => self.colours.filetypes.device,
+            f if f.is_socket()           => self.colours.filetypes.socket,
+            f if !f.is_file()            => self.colours.filetypes.special,
+            f if f.is_immediate()        => self.colours.filetypes.immediate,
+            f if f.is_image()            => self.colours.filetypes.image,
+            f if f.is_video()            => self.colours.filetypes.video,
+            f if f.is_music()            => self.colours.filetypes.music,
+            f if f.is_lossless()         => self.colours.filetypes.lossless,
+            f if f.is_crypto()           => self.colours.filetypes.crypto,
+            f if f.is_document()         => self.colours.filetypes.document,
+            f if f.is_compressed()       => self.colours.filetypes.compressed,
+            f if f.is_temp()             => self.colours.filetypes.temp,
+            f if f.is_compiled()         => self.colours.filetypes.compiled,
+            _                            => self.colours.filetypes.normal,
+        }
+    }
+}
+
+
+/// When displaying a file name, there needs to be some way to handle broken
+/// links, depending on how long the resulting Cell can be.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum LinkStyle {
+
+    /// Just display the file names, but colour them differently if they’re
+    /// a broken link or can’t be followed.
+    JustFilenames,
+
+    /// Display all files in their usual style, but follow each link with an
+    /// arrow pointing to their path, colouring the path differently if it’s
+    /// a broken link, and doing nothing if it can’t be followed.
+    FullLinkPaths,
+}
+
+
+/// Whether to append file class characters to the file names.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum Classify {
+
+    /// Just display the file names, without any characters.
+    JustFilenames,
+
+    /// Add a character after the file name depending on what class of file
+    /// it is.
+    AddFileIndicators,
+}
+
+impl Default for Classify {
+    fn default() -> Classify {
+        Classify::JustFilenames
+    }
+}

+ 7 - 12
src/output/grid.rs

@@ -3,9 +3,8 @@ use std::io::{Write, Result as IOResult};
 use term_grid as grid;
 
 use fs::File;
-use output::DisplayWidth;
 use output::colours::Colours;
-use super::filename;
+use output::file_name::{FileName, LinkStyle, Classify};
 
 
 #[derive(PartialEq, Debug, Copy, Clone)]
@@ -13,7 +12,7 @@ pub struct Grid {
     pub across: bool,
     pub console_width: usize,
     pub colours: Colours,
-    pub classify: bool,
+    pub classify: Classify,
 }
 
 impl Grid {
@@ -29,16 +28,11 @@ impl Grid {
         grid.reserve(files.len());
 
         for file in files.iter() {
-            let mut width = DisplayWidth::from_file(file, self.classify);
-
-            if file.dir.is_none() {
-                if let Some(parent) = file.path.parent() {
-                    width = width + 1 + DisplayWidth::from(parent.to_string_lossy().as_ref());
-                }
-            }
+            let filename = FileName::new(file, LinkStyle::JustFilenames, self.classify, &self.colours).paint();
+            let width = filename.width();
 
             grid.add(grid::Cell {
-                contents:  filename(file, &self.colours, false, self.classify).strings().to_string(),
+                contents:  filename.strings().to_string(),
                 width:     *width,
             });
         }
@@ -49,7 +43,8 @@ impl Grid {
         else {
             // File names too long for a grid - drop down to just listing them!
             for file in files.iter() {
-                writeln!(w, "{}", filename(file, &self.colours, false, self.classify).strings())?;
+                let name_cell = FileName::new(file, LinkStyle::JustFilenames, self.classify, &self.colours).paint();
+                writeln!(w, "{}", name_cell.strings())?;
             }
             Ok(())
         }

+ 3 - 1
src/output/grid_details.rs

@@ -12,6 +12,8 @@ use output::cell::TextCell;
 use output::column::Column;
 use output::details::{Details, Table, Environment};
 use output::grid::Grid;
+use output::file_name::LinkStyle;
+
 
 #[derive(PartialEq, Debug, Clone)]
 pub struct GridDetails {
@@ -45,7 +47,7 @@ impl GridDetails {
                               .collect::<Vec<_>>();
 
             let file_names = files.into_iter()
-                                  .map(|file| first_table.filename_cell(file, false))
+                                  .map(|file| first_table.filename(file, LinkStyle::JustFilenames).promote())
                                   .collect::<Vec<_>>();
 
             (cells, file_names)

+ 4 - 3
src/output/lines.rs

@@ -4,21 +4,22 @@ use ansi_term::ANSIStrings;
 
 use fs::File;
 
-use super::filename;
+use output::file_name::{FileName, LinkStyle, Classify};
 use super::colours::Colours;
 
 
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub struct Lines {
     pub colours: Colours,
-    pub classify: bool,
+    pub classify: Classify,
 }
 
 /// The lines view literally just displays each file, line-by-line.
 impl Lines {
     pub fn view<W: Write>(&self, files: Vec<File>, w: &mut W) -> IOResult<()> {
         for file in files {
-            writeln!(w, "{}", ANSIStrings(&filename(&file, &self.colours, true, self.classify)))?;
+            let name_cell = FileName::new(&file, LinkStyle::FullLinkPaths, self.classify, &self.colours).paint();
+            writeln!(w, "{}", ANSIStrings(&name_cell))?;
         }
         Ok(())
     }

+ 3 - 102
src/output/mod.rs

@@ -1,13 +1,10 @@
-use ansi_term::Style;
-
-use fs::{File, FileTarget};
-
 pub use self::cell::{TextCell, TextCellContents, DisplayWidth};
 pub use self::colours::Colours;
 pub use self::details::Details;
 pub use self::grid_details::GridDetails;
 pub use self::grid::Grid;
 pub use self::lines::Lines;
+pub use self::escape::escape;
 
 mod grid;
 pub mod details;
@@ -17,101 +14,5 @@ pub mod column;
 mod cell;
 mod colours;
 mod tree;
-
-
-pub fn filename(file: &File, colours: &Colours, links: bool, classify: bool) -> TextCellContents {
-    let mut bits = Vec::new();
-
-    if file.dir.is_none() {
-        if let Some(parent) = file.path.parent() {
-            let coconut = parent.components().count();
-
-            if coconut == 1 && parent.has_root() {
-                bits.push(colours.symlink_path.paint("/"));
-            }
-            else if coconut >= 1 {
-                bits.push(colours.symlink_path.paint(parent.to_string_lossy().to_string()));
-                bits.push(colours.symlink_path.paint("/"));
-            }
-        }
-    }
-
-    if !file.name.is_empty() {
-        bits.push(file_colour(colours, file).paint(file.name.clone()));
-    }
-
-    if links && file.is_link() {
-        match file.link_target() {
-            FileTarget::Ok(target) => {
-                bits.push(Style::default().paint(" "));
-                bits.push(colours.punctuation.paint("->"));
-                bits.push(Style::default().paint(" "));
-
-                if let Some(parent) = target.path.parent() {
-                    let coconut = parent.components().count();
-
-                    if coconut == 1 && parent.has_root() {
-                        bits.push(colours.symlink_path.paint("/"));
-                    }
-                    else if coconut >= 1 {
-                        bits.push(colours.symlink_path.paint(parent.to_string_lossy().to_string()));
-                        bits.push(colours.symlink_path.paint("/"));
-                    }
-                }
-
-                if !target.name.is_empty() {
-                    bits.push(file_colour(colours, &target).paint(target.name));
-                }
-            },
-
-            FileTarget::Broken(broken_path) => {
-                bits.push(Style::default().paint(" "));
-                bits.push(colours.broken_arrow.paint("->"));
-                bits.push(Style::default().paint(" "));
-                bits.push(colours.broken_filename.paint(broken_path.display().to_string()));
-            },
-
-            FileTarget::Err(_) => {
-                // Do nothing -- the error gets displayed on the next line
-            }
-        }
-    } else if classify {
-        if file.is_executable_file() {
-            bits.push(Style::default().paint("*"));
-        } else if file.is_directory() {
-            bits.push(Style::default().paint("/"));
-        } else if file.is_pipe() {
-            bits.push(Style::default().paint("|"));
-        } else if file.is_link() {
-            bits.push(Style::default().paint("@"));
-        } else if file.is_socket() {
-            bits.push(Style::default().paint("="));
-        }
-    }
-
-    bits.into()
-}
-
-pub fn file_colour(colours: &Colours, file: &File) -> Style {
-    match file {
-        f if f.is_directory()        => colours.filetypes.directory,
-        f if f.is_executable_file()  => colours.filetypes.executable,
-        f if f.is_link()             => colours.filetypes.symlink,
-        f if f.is_pipe()             => colours.filetypes.pipe,
-        f if f.is_char_device()
-           | f.is_block_device()     => colours.filetypes.device,
-        f if f.is_socket()           => colours.filetypes.socket,
-        f if !f.is_file()            => colours.filetypes.special,
-        f if f.is_immediate()        => colours.filetypes.immediate,
-        f if f.is_image()            => colours.filetypes.image,
-        f if f.is_video()            => colours.filetypes.video,
-        f if f.is_music()            => colours.filetypes.music,
-        f if f.is_lossless()         => colours.filetypes.lossless,
-        f if f.is_crypto()           => colours.filetypes.crypto,
-        f if f.is_document()         => colours.filetypes.document,
-        f if f.is_compressed()       => colours.filetypes.compressed,
-        f if f.is_temp()             => colours.filetypes.temp,
-        f if f.is_compiled()         => colours.filetypes.compiled,
-        _                            => colours.filetypes.normal,
-    }
-}
+pub mod file_name;
+mod escape;

+ 6 - 0
xtests/file_names

@@ -0,0 +1,6 @@
+ansi: [\u{1b}[34mblue\u{1b}[0m]  form-feed: [\u{c}]      new-line-dir: [\n]
+ascii: hello                     invalid-utf8-1: [�]     new-line: [\n]
+backspace: [\u{8}]               invalid-utf8-2: [�(]    return: [\r]
+bell: [\u{7}]                    invalid-utf8-3: [�(]    tab: [\t]
+emoji: [🆒]                       invalid-utf8-4: [�(�(]  utf-8: pâté
+escape: [\u{1b}]                 links                   vertical-tab: [\u{b}]

+ 18 - 0
xtests/file_names_1

@@ -0,0 +1,18 @@
+ansi: [\u{1b}[34mblue\u{1b}[0m]
+ascii: hello
+backspace: [\u{8}]
+bell: [\u{7}]
+emoji: [🆒]
+escape: [\u{1b}]
+form-feed: [\u{c}]
+invalid-utf8-1: [�]
+invalid-utf8-2: [�(]
+invalid-utf8-3: [�(]
+invalid-utf8-4: [�(�(]
+links
+new-line-dir: [\n]
+new-line: [\n]
+return: [\r]
+tab: [\t]
+utf-8: pâté
+vertical-tab: [\u{b}]

+ 12 - 0
xtests/file_names_R

@@ -0,0 +1,12 @@
+ansi: [\u{1b}[34mblue\u{1b}[0m]  form-feed: [\u{c}]      new-line-dir: [\n]
+ascii: hello                     invalid-utf8-1: [�]     new-line: [\n]
+backspace: [\u{8}]               invalid-utf8-2: [�(]    return: [\r]
+bell: [\u{7}]                    invalid-utf8-3: [�(]    tab: [\t]
+emoji: [🆒]                       invalid-utf8-4: [�(�(]  utf-8: pâté
+escape: [\u{1b}]                 links                   vertical-tab: [\u{b}]
+
+/testcases/file-names/links:
+another: [\n]  broken  subfile
+
+/testcases/file-names/new-line-dir: [\n]:
+another: [\n]  subfile

+ 29 - 0
xtests/file_names_T

@@ -0,0 +1,29 @@
+/testcases/file-names
+├── ansi: [\u{1b}[34mblue\u{1b}[0m]
+├── ascii: hello
+├── backspace: [\u{8}]
+├── bell: [\u{7}]
+├── emoji: [🆒]
+├── escape: [\u{1b}]
+├── form-feed: [\u{c}]
+├── invalid-utf8-1: [�]
+│  └── <Error: path somehow contained a NUL?>
+├── invalid-utf8-2: [�(]
+│  └── <Error: path somehow contained a NUL?>
+├── invalid-utf8-3: [�(]
+│  └── <Error: path somehow contained a NUL?>
+├── invalid-utf8-4: [�(�(]
+│  └── <Error: path somehow contained a NUL?>
+├── links
+│  ├── another: [\n] -> /testcases/file-names/new-line-dir: [\n]/another: [\n]
+│  ├── broken -> /testcases/file-names/new-line-dir: [\n]/broken
+│  │  └── <No such file or directory (os error 2)>
+│  └── subfile -> /testcases/file-names/new-line-dir: [\n]/subfile
+├── new-line-dir: [\n]
+│  ├── another: [\n]
+│  └── subfile
+├── new-line: [\n]
+├── return: [\r]
+├── tab: [\t]
+├── utf-8: pâté
+└── vertical-tab: [\u{b}]

+ 6 - 0
xtests/file_names_x

@@ -0,0 +1,6 @@
+ansi: [\u{1b}[34mblue\u{1b}[0m]  ascii: hello            backspace: [\u{8}]
+bell: [\u{7}]                    emoji: [🆒]              escape: [\u{1b}]
+form-feed: [\u{c}]               invalid-utf8-1: [�]     invalid-utf8-2: [�(]
+invalid-utf8-3: [�(]             invalid-utf8-4: [�(�(]  links
+new-line-dir: [\n]               new-line: [\n]          return: [\r]
+tab: [\t]                        utf-8: pâté             vertical-tab: [\u{b}]

+ 13 - 0
xtests/files_star_100

@@ -0,0 +1,13 @@
+/testcases/files/10_bytes  /testcases/files/1_KiB    /testcases/files/5_MiB
+/testcases/files/10_KiB    /testcases/files/1_MiB    /testcases/files/6_bytes
+/testcases/files/10_MiB    /testcases/files/2_bytes  /testcases/files/6_KiB
+/testcases/files/11_bytes  /testcases/files/2_KiB    /testcases/files/6_MiB
+/testcases/files/11_KiB    /testcases/files/2_MiB    /testcases/files/7_bytes
+/testcases/files/11_MiB    /testcases/files/3_bytes  /testcases/files/7_KiB
+/testcases/files/12_bytes  /testcases/files/3_KiB    /testcases/files/7_MiB
+/testcases/files/12_KiB    /testcases/files/3_MiB    /testcases/files/8_bytes
+/testcases/files/12_MiB    /testcases/files/4_bytes  /testcases/files/8_KiB
+/testcases/files/13_bytes  /testcases/files/4_KiB    /testcases/files/8_MiB
+/testcases/files/13_KiB    /testcases/files/4_MiB    /testcases/files/9_bytes
+/testcases/files/13_MiB    /testcases/files/5_bytes  /testcases/files/9_KiB
+/testcases/files/1_bytes   /testcases/files/5_KiB    /testcases/files/9_MiB

+ 8 - 0
xtests/files_star_150

@@ -0,0 +1,8 @@
+/testcases/files/10_bytes  /testcases/files/12_MiB    /testcases/files/2_KiB    /testcases/files/5_bytes  /testcases/files/7_MiB
+/testcases/files/10_KiB    /testcases/files/13_bytes  /testcases/files/2_MiB    /testcases/files/5_KiB    /testcases/files/8_bytes
+/testcases/files/10_MiB    /testcases/files/13_KiB    /testcases/files/3_bytes  /testcases/files/5_MiB    /testcases/files/8_KiB
+/testcases/files/11_bytes  /testcases/files/13_MiB    /testcases/files/3_KiB    /testcases/files/6_bytes  /testcases/files/8_MiB
+/testcases/files/11_KiB    /testcases/files/1_bytes   /testcases/files/3_MiB    /testcases/files/6_KiB    /testcases/files/9_bytes
+/testcases/files/11_MiB    /testcases/files/1_KiB     /testcases/files/4_bytes  /testcases/files/6_MiB    /testcases/files/9_KiB
+/testcases/files/12_bytes  /testcases/files/1_MiB     /testcases/files/4_KiB    /testcases/files/7_bytes  /testcases/files/9_MiB
+/testcases/files/12_KiB    /testcases/files/2_bytes   /testcases/files/4_MiB    /testcases/files/7_KiB    

+ 6 - 0
xtests/files_star_200

@@ -0,0 +1,6 @@
+/testcases/files/10_bytes  /testcases/files/12_bytes  /testcases/files/1_bytes  /testcases/files/3_bytes  /testcases/files/5_bytes  /testcases/files/7_bytes  /testcases/files/9_bytes
+/testcases/files/10_KiB    /testcases/files/12_KiB    /testcases/files/1_KiB    /testcases/files/3_KiB    /testcases/files/5_KiB    /testcases/files/7_KiB    /testcases/files/9_KiB
+/testcases/files/10_MiB    /testcases/files/12_MiB    /testcases/files/1_MiB    /testcases/files/3_MiB    /testcases/files/5_MiB    /testcases/files/7_MiB    /testcases/files/9_MiB
+/testcases/files/11_bytes  /testcases/files/13_bytes  /testcases/files/2_bytes  /testcases/files/4_bytes  /testcases/files/6_bytes  /testcases/files/8_bytes  
+/testcases/files/11_KiB    /testcases/files/13_KiB    /testcases/files/2_KiB    /testcases/files/4_KiB    /testcases/files/6_KiB    /testcases/files/8_KiB    
+/testcases/files/11_MiB    /testcases/files/13_MiB    /testcases/files/2_MiB    /testcases/files/4_MiB    /testcases/files/6_MiB    /testcases/files/8_MiB    

+ 39 - 0
xtests/files_star_lG_100

@@ -0,0 +1,39 @@
+.rw-r--r--   10 cassowary  1 Jan 12:34 /testcases/files/10_bytes
+.rw-r--r--  10k cassowary  1 Jan 12:34 /testcases/files/10_KiB
+.rw-r--r--  10M cassowary  1 Jan 12:34 /testcases/files/10_MiB
+.rw-r--r--   11 cassowary  1 Jan 12:34 /testcases/files/11_bytes
+.rw-r--r--  11k cassowary  1 Jan 12:34 /testcases/files/11_KiB
+.rw-r--r--  11M cassowary  1 Jan 12:34 /testcases/files/11_MiB
+.rw-r--r--   12 cassowary  1 Jan 12:34 /testcases/files/12_bytes
+.rw-r--r--  12k cassowary  1 Jan 12:34 /testcases/files/12_KiB
+.rw-r--r--  12M cassowary  1 Jan 12:34 /testcases/files/12_MiB
+.rw-r--r--   13 cassowary  1 Jan 12:34 /testcases/files/13_bytes
+.rw-r--r--  13k cassowary  1 Jan 12:34 /testcases/files/13_KiB
+.rw-r--r--  13M cassowary  1 Jan 12:34 /testcases/files/13_MiB
+.rw-r--r--    1 cassowary  1 Jan 12:34 /testcases/files/1_bytes
+.rw-r--r-- 1.0k cassowary  1 Jan 12:34 /testcases/files/1_KiB
+.rw-r--r-- 1.0M cassowary  1 Jan 12:34 /testcases/files/1_MiB
+.rw-r--r--    2 cassowary  1 Jan 12:34 /testcases/files/2_bytes
+.rw-r--r-- 2.0k cassowary  1 Jan 12:34 /testcases/files/2_KiB
+.rw-r--r-- 2.1M cassowary  1 Jan 12:34 /testcases/files/2_MiB
+.rw-r--r--    3 cassowary  1 Jan 12:34 /testcases/files/3_bytes
+.rw-r--r-- 3.1k cassowary  1 Jan 12:34 /testcases/files/3_KiB
+.rw-r--r-- 3.1M cassowary  1 Jan 12:34 /testcases/files/3_MiB
+.rw-r--r--    4 cassowary  1 Jan 12:34 /testcases/files/4_bytes
+.rw-r--r-- 4.1k cassowary  1 Jan 12:34 /testcases/files/4_KiB
+.rw-r--r-- 4.2M cassowary  1 Jan 12:34 /testcases/files/4_MiB
+.rw-r--r--    5 cassowary  1 Jan 12:34 /testcases/files/5_bytes
+.rw-r--r-- 5.1k cassowary  1 Jan 12:34 /testcases/files/5_KiB
+.rw-r--r-- 5.2M cassowary  1 Jan 12:34 /testcases/files/5_MiB
+.rw-r--r--    6 cassowary  1 Jan 12:34 /testcases/files/6_bytes
+.rw-r--r-- 6.1k cassowary  1 Jan 12:34 /testcases/files/6_KiB
+.rw-r--r-- 6.3M cassowary  1 Jan 12:34 /testcases/files/6_MiB
+.rw-r--r--    7 cassowary  1 Jan 12:34 /testcases/files/7_bytes
+.rw-r--r-- 7.2k cassowary  1 Jan 12:34 /testcases/files/7_KiB
+.rw-r--r-- 7.3M cassowary  1 Jan 12:34 /testcases/files/7_MiB
+.rw-r--r--    8 cassowary  1 Jan 12:34 /testcases/files/8_bytes
+.rw-r--r-- 8.2k cassowary  1 Jan 12:34 /testcases/files/8_KiB
+.rw-r--r-- 8.4M cassowary  1 Jan 12:34 /testcases/files/8_MiB
+.rw-r--r--    9 cassowary  1 Jan 12:34 /testcases/files/9_bytes
+.rw-r--r-- 9.2k cassowary  1 Jan 12:34 /testcases/files/9_KiB
+.rw-r--r-- 9.4M cassowary  1 Jan 12:34 /testcases/files/9_MiB

+ 20 - 0
xtests/files_star_lG_150

@@ -0,0 +1,20 @@
+.rw-r--r--   10 cassowary  1 Jan 12:34 /testcases/files/10_bytes    .rw-r--r-- 3.1M cassowary  1 Jan 12:34 /testcases/files/3_MiB
+.rw-r--r--  10k cassowary  1 Jan 12:34 /testcases/files/10_KiB      .rw-r--r--    4 cassowary  1 Jan 12:34 /testcases/files/4_bytes
+.rw-r--r--  10M cassowary  1 Jan 12:34 /testcases/files/10_MiB      .rw-r--r-- 4.1k cassowary  1 Jan 12:34 /testcases/files/4_KiB
+.rw-r--r--   11 cassowary  1 Jan 12:34 /testcases/files/11_bytes    .rw-r--r-- 4.2M cassowary  1 Jan 12:34 /testcases/files/4_MiB
+.rw-r--r--  11k cassowary  1 Jan 12:34 /testcases/files/11_KiB      .rw-r--r--    5 cassowary  1 Jan 12:34 /testcases/files/5_bytes
+.rw-r--r--  11M cassowary  1 Jan 12:34 /testcases/files/11_MiB      .rw-r--r-- 5.1k cassowary  1 Jan 12:34 /testcases/files/5_KiB
+.rw-r--r--   12 cassowary  1 Jan 12:34 /testcases/files/12_bytes    .rw-r--r-- 5.2M cassowary  1 Jan 12:34 /testcases/files/5_MiB
+.rw-r--r--  12k cassowary  1 Jan 12:34 /testcases/files/12_KiB      .rw-r--r--    6 cassowary  1 Jan 12:34 /testcases/files/6_bytes
+.rw-r--r--  12M cassowary  1 Jan 12:34 /testcases/files/12_MiB      .rw-r--r-- 6.1k cassowary  1 Jan 12:34 /testcases/files/6_KiB
+.rw-r--r--   13 cassowary  1 Jan 12:34 /testcases/files/13_bytes    .rw-r--r-- 6.3M cassowary  1 Jan 12:34 /testcases/files/6_MiB
+.rw-r--r--  13k cassowary  1 Jan 12:34 /testcases/files/13_KiB      .rw-r--r--    7 cassowary  1 Jan 12:34 /testcases/files/7_bytes
+.rw-r--r--  13M cassowary  1 Jan 12:34 /testcases/files/13_MiB      .rw-r--r-- 7.2k cassowary  1 Jan 12:34 /testcases/files/7_KiB
+.rw-r--r--    1 cassowary  1 Jan 12:34 /testcases/files/1_bytes     .rw-r--r-- 7.3M cassowary  1 Jan 12:34 /testcases/files/7_MiB
+.rw-r--r-- 1.0k cassowary  1 Jan 12:34 /testcases/files/1_KiB       .rw-r--r--    8 cassowary  1 Jan 12:34 /testcases/files/8_bytes
+.rw-r--r-- 1.0M cassowary  1 Jan 12:34 /testcases/files/1_MiB       .rw-r--r-- 8.2k cassowary  1 Jan 12:34 /testcases/files/8_KiB
+.rw-r--r--    2 cassowary  1 Jan 12:34 /testcases/files/2_bytes     .rw-r--r-- 8.4M cassowary  1 Jan 12:34 /testcases/files/8_MiB
+.rw-r--r-- 2.0k cassowary  1 Jan 12:34 /testcases/files/2_KiB       .rw-r--r--    9 cassowary  1 Jan 12:34 /testcases/files/9_bytes
+.rw-r--r-- 2.1M cassowary  1 Jan 12:34 /testcases/files/2_MiB       .rw-r--r-- 9.2k cassowary  1 Jan 12:34 /testcases/files/9_KiB
+.rw-r--r--    3 cassowary  1 Jan 12:34 /testcases/files/3_bytes     .rw-r--r-- 9.4M cassowary  1 Jan 12:34 /testcases/files/9_MiB
+.rw-r--r-- 3.1k cassowary  1 Jan 12:34 /testcases/files/3_KiB       

+ 13 - 0
xtests/files_star_lG_200

@@ -0,0 +1,13 @@
+.rw-r--r--  10 cassowary  1 Jan 12:34 /testcases/files/10_bytes    .rw-r--r-- 1.0k cassowary  1 Jan 12:34 /testcases/files/1_KiB      .rw-r--r-- 5.2M cassowary  1 Jan 12:34 /testcases/files/5_MiB
+.rw-r--r-- 10k cassowary  1 Jan 12:34 /testcases/files/10_KiB      .rw-r--r-- 1.0M cassowary  1 Jan 12:34 /testcases/files/1_MiB      .rw-r--r--    6 cassowary  1 Jan 12:34 /testcases/files/6_bytes
+.rw-r--r-- 10M cassowary  1 Jan 12:34 /testcases/files/10_MiB      .rw-r--r--    2 cassowary  1 Jan 12:34 /testcases/files/2_bytes    .rw-r--r-- 6.1k cassowary  1 Jan 12:34 /testcases/files/6_KiB
+.rw-r--r--  11 cassowary  1 Jan 12:34 /testcases/files/11_bytes    .rw-r--r-- 2.0k cassowary  1 Jan 12:34 /testcases/files/2_KiB      .rw-r--r-- 6.3M cassowary  1 Jan 12:34 /testcases/files/6_MiB
+.rw-r--r-- 11k cassowary  1 Jan 12:34 /testcases/files/11_KiB      .rw-r--r-- 2.1M cassowary  1 Jan 12:34 /testcases/files/2_MiB      .rw-r--r--    7 cassowary  1 Jan 12:34 /testcases/files/7_bytes
+.rw-r--r-- 11M cassowary  1 Jan 12:34 /testcases/files/11_MiB      .rw-r--r--    3 cassowary  1 Jan 12:34 /testcases/files/3_bytes    .rw-r--r-- 7.2k cassowary  1 Jan 12:34 /testcases/files/7_KiB
+.rw-r--r--  12 cassowary  1 Jan 12:34 /testcases/files/12_bytes    .rw-r--r-- 3.1k cassowary  1 Jan 12:34 /testcases/files/3_KiB      .rw-r--r-- 7.3M cassowary  1 Jan 12:34 /testcases/files/7_MiB
+.rw-r--r-- 12k cassowary  1 Jan 12:34 /testcases/files/12_KiB      .rw-r--r-- 3.1M cassowary  1 Jan 12:34 /testcases/files/3_MiB      .rw-r--r--    8 cassowary  1 Jan 12:34 /testcases/files/8_bytes
+.rw-r--r-- 12M cassowary  1 Jan 12:34 /testcases/files/12_MiB      .rw-r--r--    4 cassowary  1 Jan 12:34 /testcases/files/4_bytes    .rw-r--r-- 8.2k cassowary  1 Jan 12:34 /testcases/files/8_KiB
+.rw-r--r--  13 cassowary  1 Jan 12:34 /testcases/files/13_bytes    .rw-r--r-- 4.1k cassowary  1 Jan 12:34 /testcases/files/4_KiB      .rw-r--r-- 8.4M cassowary  1 Jan 12:34 /testcases/files/8_MiB
+.rw-r--r-- 13k cassowary  1 Jan 12:34 /testcases/files/13_KiB      .rw-r--r-- 4.2M cassowary  1 Jan 12:34 /testcases/files/4_MiB      .rw-r--r--    9 cassowary  1 Jan 12:34 /testcases/files/9_bytes
+.rw-r--r-- 13M cassowary  1 Jan 12:34 /testcases/files/13_MiB      .rw-r--r--    5 cassowary  1 Jan 12:34 /testcases/files/5_bytes    .rw-r--r-- 9.2k cassowary  1 Jan 12:34 /testcases/files/9_KiB
+.rw-r--r--   1 cassowary  1 Jan 12:34 /testcases/files/1_bytes     .rw-r--r-- 5.1k cassowary  1 Jan 12:34 /testcases/files/5_KiB      .rw-r--r-- 9.4M cassowary  1 Jan 12:34 /testcases/files/9_MiB

+ 2 - 2
xtests/links

@@ -1,2 +1,2 @@
-broken       forbidden  parent_dir  some_file           some_file_relative
-current_dir  itself     root        some_file_absolute  usr
+broken       forbidden  parent_dir  some_file           some_file_relative
+current_dir  itself     root        some_file_absolute  usr

+ 15 - 0
xtests/run.sh

@@ -33,6 +33,10 @@ COLUMNS=120 $exa $testcases/files | diff -q - $results/files_120  || exit 1
 COLUMNS=160 $exa $testcases/files | diff -q - $results/files_160  || exit 1
 COLUMNS=200 $exa $testcases/files | diff -q - $results/files_200  || exit 1
 
+COLUMNS=100 $exa $testcases/files/* | diff -q - $results/files_star_100   || exit 1
+COLUMNS=150 $exa $testcases/files/* | diff -q - $results/files_star_150  || exit 1
+COLUMNS=200 $exa $testcases/files/* | diff -q - $results/files_star_200  || exit 1
+
 
 # Long grid view tests
 COLUMNS=40  $exa $testcases/files -lG | diff -q - $results/files_lG_40   || exit 1
@@ -41,6 +45,10 @@ COLUMNS=120 $exa $testcases/files -lG | diff -q - $results/files_lG_120  || exit
 COLUMNS=160 $exa $testcases/files -lG | diff -q - $results/files_lG_160  || exit 1
 COLUMNS=200 $exa $testcases/files -lG | diff -q - $results/files_lG_200  || exit 1
 
+COLUMNS=100 $exa $testcases/files/* -lG | diff -q - $results/files_star_lG_100  || exit 1
+COLUMNS=150 $exa $testcases/files/* -lG | diff -q - $results/files_star_lG_150  || exit 1
+COLUMNS=200 $exa $testcases/files/* -lG | diff -q - $results/files_star_lG_200  || exit 1
+
 
 # Attributes
 $exa $testcases/attributes -l@T | diff -q - $results/attributes  || exit 1
@@ -54,6 +62,13 @@ $exa $testcases/passwd -lgh | diff -q - $results/passwd  || exit 1
 sudo -u cassowary $exa $testcases/permissions -lghR 2>&1 | diff -q - $results/permissions_sudo  || exit 1
                   $exa $testcases/permissions -lghR 2>&1 | diff -q - $results/permissions       || exit 1
 
+# File names
+# (Mostly escaping control characters in file names)
+COLUMNS=80 $exa $testcases/file-names    2>&1 | diff -q - $results/file_names   || exit 1
+COLUMNS=80 $exa $testcases/file-names -x 2>&1 | diff -q - $results/file_names_x || exit 1
+COLUMNS=80 $exa $testcases/file-names -R 2>&1 | diff -q - $results/file_names_R || exit 1
+           $exa $testcases/file-names -1 2>&1 | diff -q - $results/file_names_1 || exit 1
+           $exa $testcases/file-names -T 2>&1 | diff -q - $results/file_names_T || exit 1
 
 # File types
 $exa $testcases/file-names-exts -1 2>&1 | diff -q - $results/file-names-exts  || exit 1