refactor: fix rustfmt issues and place skips where needed

Signed-off-by: Sandro-Alessio Gierens <sandro@gierens.de>
This commit is contained in:
Sandro-Alessio Gierens 2023-09-22 18:40:30 +02:00
parent 54c8dae733
commit f555d42972
58 changed files with 2106 additions and 1600 deletions

View File

@ -1,7 +1,11 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
pub fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("logger", |b| b.iter(|| eza::logger::configure(black_box(std::env::var_os(eza::options::vars::EZA_DEBUG)))));
c.bench_function("logger", |b| {
b.iter(|| {
eza::logger::configure(black_box(std::env::var_os(eza::options::vars::EZA_DEBUG)))
})
});
}
criterion_group!(benches, criterion_benchmark);

View File

@ -9,7 +9,6 @@
///
/// - https://stackoverflow.com/q/43753491/3484614
/// - https://crates.io/crates/vergen
use std::env;
use std::fs::File;
use std::io::{self, Write};
@ -17,31 +16,40 @@ use std::path::PathBuf;
use chrono::prelude::*;
/// The build script entry point.
fn main() -> io::Result<()> {
#![allow(clippy::write_with_newline)]
let tagline = "eza - A modern, maintained replacement for ls";
let url = "https://github.com/eza-community/eza";
let url = "https://github.com/eza-community/eza";
let ver =
if is_debug_build() {
format!("{}\nv{} \\1;31m(pre-release debug build!)\\0m\n\\1;4;34m{}\\0m", tagline, version_string(), url)
}
else if is_development_version() {
format!("{}\nv{} [{}] built on {} \\1;31m(pre-release!)\\0m\n\\1;4;34m{}\\0m", tagline, version_string(), git_hash(), build_date(), url)
}
else {
format!("{}\nv{}\n\\1;4;34m{}\\0m", tagline, version_string(), url)
};
let ver = if is_debug_build() {
format!(
"{}\nv{} \\1;31m(pre-release debug build!)\\0m\n\\1;4;34m{}\\0m",
tagline,
version_string(),
url
)
} else if is_development_version() {
format!(
"{}\nv{} [{}] built on {} \\1;31m(pre-release!)\\0m\n\\1;4;34m{}\\0m",
tagline,
version_string(),
git_hash(),
build_date(),
url
)
} else {
format!("{}\nv{}\n\\1;4;34m{}\\0m", tagline, version_string(), url)
};
// We need to create these files in the Cargo output directory.
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
let path = &out.join("version_string.txt");
// Bland version text
let mut f = File::create(path).unwrap_or_else(|_| { panic!("{}", path.to_string_lossy().to_string()) });
let mut f =
File::create(path).unwrap_or_else(|_| panic!("{}", path.to_string_lossy().to_string()));
writeln!(f, "{}", strip_codes(&ver))?;
Ok(())
@ -49,9 +57,10 @@ fn main() -> io::Result<()> {
/// Removes escape codes from a string.
fn strip_codes(input: &str) -> String {
input.replace("\\0m", "")
.replace("\\1;31m", "")
.replace("\\1;4;34m", "")
input
.replace("\\0m", "")
.replace("\\1;31m", "")
.replace("\\1;4;34m", "")
}
/// Retrieve the projects current Git hash, as a string.
@ -61,8 +70,12 @@ fn git_hash() -> String {
String::from_utf8_lossy(
&Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output().unwrap()
.stdout).trim().to_string()
.output()
.unwrap()
.stdout,
)
.trim()
.to_string()
}
/// Whether we should show pre-release info in the version string.
@ -88,7 +101,7 @@ fn version_string() -> String {
let mut ver = cargo_version();
let feats = nonstandard_features_string();
if ! feats.is_empty() {
if !feats.is_empty() {
ver.push_str(&format!(" [{}]", &feats));
}
@ -98,7 +111,7 @@ fn version_string() -> String {
/// Finds whether a feature is enabled by examining the Cargo variable.
fn feature_enabled(name: &str) -> bool {
env::var(format!("CARGO_FEATURE_{}", name))
.map(|e| ! e.is_empty())
.map(|e| !e.is_empty())
.unwrap_or(false)
}
@ -108,8 +121,7 @@ fn nonstandard_features_string() -> String {
if feature_enabled("GIT") {
s.push("+git");
}
else {
} else {
s.push("-git");
}

View File

@ -1,7 +1,7 @@
use crate::fs::feature::git::GitCache;
use crate::fs::fields::GitStatus;
use std::io;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::slice::Iter as SliceIter;
@ -9,7 +9,6 @@ use log::*;
use crate::fs::File;
/// A **Dir** provides a cached list of the file paths in a directory thats
/// being listed.
///
@ -17,7 +16,6 @@ use crate::fs::File;
/// check the existence of surrounding files, then highlight themselves
/// accordingly. (See `File#get_source_files`)
pub struct Dir {
/// A vector of the files that have been read from this directory.
contents: Vec<PathBuf>,
@ -26,7 +24,6 @@ pub struct Dir {
}
impl Dir {
/// Create a new Dir object filled with all the files in the directory
/// pointed to by the given path. Fails if the directory cant be read, or
/// isnt actually a directory, or if theres an IO error that occurs at
@ -39,8 +36,8 @@ impl Dir {
info!("Reading directory {:?}", &path);
let contents = fs::read_dir(&path)?
.map(|result| result.map(|entry| entry.path()))
.collect::<Result<_, _>>()?;
.map(|result| result.map(|entry| entry.path()))
.collect::<Result<_, _>>()?;
info!("Read directory success {:?}", &path);
Ok(Self { contents, path })
@ -48,12 +45,18 @@ impl Dir {
/// Produce an iterator of IO results of trying to read all the files in
/// this directory.
pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, git: Option<&'ig GitCache>, git_ignoring: bool, deref_links: bool) -> Files<'dir, 'ig> {
pub fn files<'dir, 'ig>(
&'dir self,
dots: DotFilter,
git: Option<&'ig GitCache>,
git_ignoring: bool,
deref_links: bool,
) -> Files<'dir, 'ig> {
Files {
inner: self.contents.iter(),
dir: self,
dotfiles: dots.shows_dotfiles(),
dots: dots.dots(),
inner: self.contents.iter(),
dir: self,
dotfiles: dots.shows_dotfiles(),
dots: dots.dots(),
git,
git_ignoring,
deref_links,
@ -71,10 +74,8 @@ impl Dir {
}
}
/// Iterator over reading the contents of a directory as `File` objects.
pub struct Files<'dir, 'ig> {
/// The internal iterator over the paths that have been read already.
inner: SliceIter<'dir, PathBuf>,
@ -112,26 +113,26 @@ impl<'dir, 'ig> Files<'dir, 'ig> {
loop {
if let Some(path) = self.inner.next() {
let filename = File::filename(path);
if ! self.dotfiles && filename.starts_with('.') {
if !self.dotfiles && filename.starts_with('.') {
continue;
}
// Also hide _prefix files on Windows because it's used by old applications
// as an alternative to dot-prefix files.
#[cfg(windows)]
if ! self.dotfiles && filename.starts_with('_') {
if !self.dotfiles && filename.starts_with('_') {
continue;
}
if self.git_ignoring {
let git_status = self.git.map(|g| g.get(path, false)).unwrap_or_default();
if git_status.unstaged == GitStatus::Ignored {
continue;
continue;
}
}
let file = File::from_args(path.clone(), self.dir, filename, self.deref_links)
.map_err(|e| (path.clone(), e));
.map_err(|e| (path.clone(), e));
// Windows has its own concept of hidden files, when dotfiles are
// hidden Windows hidden files should also be filtered out
@ -143,7 +144,7 @@ impl<'dir, 'ig> Files<'dir, 'ig> {
return Some(file);
}
return None
return None;
}
}
}
@ -151,7 +152,6 @@ impl<'dir, 'ig> Files<'dir, 'ig> {
/// The dot directories that need to be listed before actual files, if any.
/// If these arent being printed, then `FilesNext` is used to skip them.
enum DotsNext {
/// List the `.` directory next.
Dot,
@ -169,30 +169,24 @@ impl<'dir, 'ig> Iterator for Files<'dir, 'ig> {
match self.dots {
DotsNext::Dot => {
self.dots = DotsNext::DotDot;
Some(File::new_aa_current(self.dir)
.map_err(|e| (Path::new(".").to_path_buf(), e)))
Some(File::new_aa_current(self.dir).map_err(|e| (Path::new(".").to_path_buf(), e)))
}
DotsNext::DotDot => {
self.dots = DotsNext::Files;
Some(File::new_aa_parent(self.parent(), self.dir)
.map_err(|e| (self.parent(), e)))
Some(File::new_aa_parent(self.parent(), self.dir).map_err(|e| (self.parent(), e)))
}
DotsNext::Files => {
self.next_visible_file()
}
DotsNext::Files => self.next_visible_file(),
}
}
}
/// Usually files in Unix use a leading dot to be hidden or visible, but two
/// entries in particular are “extra-hidden”: `.` and `..`, which only become
/// visible after an extra `-a` option.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum DotFilter {
/// Shows files, dotfiles, and `.` and `..`.
DotfilesAndDots,
@ -210,12 +204,11 @@ impl Default for DotFilter {
}
impl DotFilter {
/// Whether this filter should show dotfiles in a listing.
fn shows_dotfiles(self) -> bool {
match self {
Self::JustFiles => false,
Self::Dotfiles => true,
Self::JustFiles => false,
Self::Dotfiles => true,
Self::DotfilesAndDots => true,
}
}
@ -223,9 +216,9 @@ impl DotFilter {
/// Whether this filter should add dot directories to a listing.
fn dots(self) -> DotsNext {
match self {
Self::JustFiles => DotsNext::Files,
Self::Dotfiles => DotsNext::Files,
Self::DotfilesAndDots => DotsNext::Dot,
Self::JustFiles => DotsNext::Files,
Self::Dotfiles => DotsNext::Files,
Self::DotfilesAndDots => DotsNext::Dot,
}
}
}

View File

@ -21,7 +21,6 @@
/// directories inline, with their contents immediately underneath.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum DirAction {
/// This directory should be listed along with the regular files, instead
/// of having its contents queried.
AsFile,
@ -37,30 +36,27 @@ pub enum DirAction {
}
impl DirAction {
/// Gets the recurse options, if this dir action has any.
pub fn recurse_options(self) -> Option<RecurseOptions> {
match self {
Self::Recurse(o) => Some(o),
_ => None,
Self::Recurse(o) => Some(o),
_ => None,
}
}
/// Whether to treat directories as regular files or not.
pub fn treat_dirs_as_files(self) -> bool {
match self {
Self::AsFile => true,
Self::Recurse(o) => o.tree,
Self::List => false,
Self::AsFile => true,
Self::Recurse(o) => o.tree,
Self::List => false,
}
}
}
/// The options that determine how to recurse into a directory.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct RecurseOptions {
/// Whether recursion should be done as a tree or as multiple individual
/// views of files.
pub tree: bool,
@ -71,12 +67,11 @@ pub struct RecurseOptions {
}
impl RecurseOptions {
/// Returns whether a directory of the given depth would be too deep.
pub fn is_too_deep(self, depth: usize) -> bool {
match self.max_depth {
None => false,
Some(d) => d <= depth
None => false,
Some(d) => d <= depth,
}
}
}

View File

@ -11,13 +11,11 @@ use log::*;
use crate::fs::fields as f;
/// A **Git cache** is assembled based on the users input arguments.
///
/// This uses vectors to avoid the overhead of hashing: its not worth it when the
/// expected number of Git repositories per exa invocation is 0 or 1...
pub struct GitCache {
/// A list of discovered Git repositories and their paths.
repos: Vec<GitRepo>,
@ -31,7 +29,8 @@ impl GitCache {
}
pub fn get(&self, index: &Path, prefix_lookup: bool) -> f::Git {
self.repos.iter()
self.repos
.iter()
.find(|repo| repo.has_path(index))
.map(|repo| repo.search(index, prefix_lookup))
.unwrap_or_default()
@ -41,7 +40,8 @@ impl GitCache {
use std::iter::FromIterator;
impl FromIterator<PathBuf> for GitCache {
fn from_iter<I>(iter: I) -> Self
where I: IntoIterator<Item=PathBuf>
where
I: IntoIterator<Item = PathBuf>,
{
let iter = iter.into_iter();
let mut git = Self {
@ -66,16 +66,17 @@ impl FromIterator<PathBuf> for GitCache {
for path in iter {
if git.misses.contains(&path) {
debug!("Skipping {:?} because it already came back Gitless", path);
}
else if git.repos.iter().any(|e| e.has_path(&path)) {
} else if git.repos.iter().any(|e| e.has_path(&path)) {
debug!("Skipping {:?} because we already queried it", path);
}
else {
} else {
let flags = git2::RepositoryOpenFlags::FROM_ENV;
match GitRepo::discover(path, flags) {
Ok(r) => {
if let Some(r2) = git.repos.iter_mut().find(|e| e.has_workdir(&r.workdir)) {
debug!("Adding to existing repo (workdir matches with {:?})", r2.workdir);
debug!(
"Adding to existing repo (workdir matches with {:?})",
r2.workdir
);
r2.extra_paths.push(r.original_path);
continue;
}
@ -94,10 +95,8 @@ impl FromIterator<PathBuf> for GitCache {
}
}
/// A **Git repository** is one weve discovered somewhere on the filesystem.
pub struct GitRepo {
/// The queryable contents of the repository: either a `git2` repo, or the
/// cached results from when we queried it last time.
contents: Mutex<GitContents>,
@ -118,11 +117,8 @@ pub struct GitRepo {
/// A repositorys queried state.
enum GitContents {
/// All the interesting Git stuff goes through this.
Before {
repo: git2::Repository,
},
Before { repo: git2::Repository },
/// Temporary value used in `repo_to_statuses` so we can move the
/// repository out of the `Before` variant.
@ -130,13 +126,10 @@ enum GitContents {
/// The data weve extracted from the repository, but only after weve
/// actually done so.
After {
statuses: Git,
},
After { statuses: Git },
}
impl GitRepo {
/// Searches through this repository for a path (to a file or directory,
/// depending on the prefix-lookup flag) and returns its Git status.
///
@ -171,7 +164,8 @@ impl GitRepo {
/// Whether this repository cares about the given path at all.
fn has_path(&self, path: &Path) -> bool {
path.starts_with(&self.original_path) || self.extra_paths.iter().any(|e| path.starts_with(e))
path.starts_with(&self.original_path)
|| self.extra_paths.iter().any(|e| path.starts_with(e))
}
/// Open a Git repository. Depending on the flags, the path is either
@ -191,16 +185,19 @@ impl GitRepo {
if let Some(workdir) = repo.workdir() {
let workdir = workdir.to_path_buf();
let contents = Mutex::new(GitContents::Before { repo });
Ok(Self { contents, workdir, original_path: path, extra_paths: Vec::new() })
}
else {
Ok(Self {
contents,
workdir,
original_path: path,
extra_paths: Vec::new(),
})
} else {
warn!("Repository has no workdir?");
Err(path)
}
}
}
impl GitContents {
/// Assumes that the repository hasnt been queried, and extracts it
/// (consuming the value) if it has. This is needed because the entire
@ -208,8 +205,7 @@ impl GitContents {
fn inner_repo(self) -> git2::Repository {
if let Self::Before { repo } = self {
repo
}
else {
} else {
unreachable!("Tried to extract a non-Repository")
}
}
@ -255,20 +251,21 @@ fn repo_to_statuses(repo: &git2::Repository, workdir: &Path) -> Git {
// Even inserting another logging line immediately afterwards doesnt make it
// look any faster.
/// Container of Git statuses for all the files in this folders Git repository.
struct Git {
statuses: Vec<(PathBuf, git2::Status)>,
}
impl Git {
/// Get either the file or directory status for the given path.
/// “Prefix lookup” means that it should report an aggregate status of all
/// paths starting with the given prefix (in other words, a directory).
fn status(&self, index: &Path, prefix_lookup: bool) -> f::Git {
if prefix_lookup { self.dir_status(index) }
else { self.file_status(index) }
if prefix_lookup {
self.dir_status(index)
} else {
self.file_status(index)
}
}
/// Get the user-facing status of a file.
@ -277,11 +274,15 @@ impl Git {
fn file_status(&self, file: &Path) -> f::Git {
let path = reorient(file);
let s = self.statuses.iter()
.filter(|p| if p.1 == git2::Status::IGNORED {
path.starts_with(&p.0)
} else {
p.0 == path
let s = self
.statuses
.iter()
.filter(|p| {
if p.1 == git2::Status::IGNORED {
path.starts_with(&p.0)
} else {
p.0 == path
}
})
.fold(git2::Status::empty(), |a, b| a | b.1);
@ -298,11 +299,15 @@ impl Git {
fn dir_status(&self, dir: &Path) -> f::Git {
let path = reorient(dir);
let s = self.statuses.iter()
.filter(|p| if p.1 == git2::Status::IGNORED {
path.starts_with(&p.0)
} else {
p.0.starts_with(&path)
let s = self
.statuses
.iter()
.filter(|p| {
if p.1 == git2::Status::IGNORED {
path.starts_with(&p.0)
} else {
p.0.starts_with(&path)
}
})
.fold(git2::Status::empty(), |a, b| a | b.1);
@ -312,7 +317,6 @@ impl Git {
}
}
/// Converts a path to an absolute path based on the current directory.
/// Paths need to be absolute for them to be compared properly, otherwise
/// youd ask a repo about “./README.md” but it only knows about
@ -323,8 +327,8 @@ fn reorient(path: &Path) -> PathBuf {
// TODO: Im not 100% on this func tbh
let path = match current_dir() {
Err(_) => Path::new(".").join(path),
Ok(dir) => dir.join(path),
Err(_) => Path::new(".").join(path),
Ok(dir) => dir.join(path),
};
path.canonicalize().unwrap_or(path)
@ -334,12 +338,17 @@ fn reorient(path: &Path) -> PathBuf {
fn reorient(path: &Path) -> PathBuf {
let unc_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
// On Windows UNC path is returned. We need to strip the prefix for it to work.
let normal_path = unc_path.as_os_str().to_str().unwrap().trim_start_matches("\\\\?\\");
let normal_path = unc_path
.as_os_str()
.to_str()
.unwrap()
.trim_start_matches("\\\\?\\");
PathBuf::from(normal_path)
}
/// The character to display if the file has been modified, but not staged.
fn working_tree_status(status: git2::Status) -> f::GitStatus {
#[rustfmt::skip]
match status {
s if s.contains(git2::Status::WT_NEW) => f::GitStatus::New,
s if s.contains(git2::Status::WT_MODIFIED) => f::GitStatus::Modified,
@ -355,6 +364,7 @@ fn working_tree_status(status: git2::Status) -> f::GitStatus {
/// The character to display if the file has been modified and the change
/// has been staged.
fn index_status(status: git2::Status) -> f::GitStatus {
#[rustfmt::skip]
match status {
s if s.contains(git2::Status::INDEX_NEW) => f::GitStatus::New,
s if s.contains(git2::Status::INDEX_MODIFIED) => f::GitStatus::Modified,
@ -365,21 +375,26 @@ fn index_status(status: git2::Status) -> f::GitStatus {
}
}
fn current_branch(repo: &git2::Repository) -> Option<String>{
fn current_branch(repo: &git2::Repository) -> Option<String> {
let head = match repo.head() {
Ok(head) => Some(head),
Err(ref e) if e.code() == git2::ErrorCode::UnbornBranch || e.code() == git2::ErrorCode::NotFound => return None,
Err(ref e)
if e.code() == git2::ErrorCode::UnbornBranch
|| e.code() == git2::ErrorCode::NotFound =>
{
return None
}
Err(e) => {
error!("Error looking up Git branch: {:?}", e);
return None
return None;
}
};
if let Some(h) = head{
if let Some(s) = h.shorthand(){
if let Some(h) = head {
if let Some(s) = h.shorthand() {
let branch_name = s.to_owned();
if branch_name.len() > 10 {
return Some(branch_name[..8].to_string()+"..");
return Some(branch_name[..8].to_string() + "..");
}
return Some(branch_name);
}

View File

@ -10,12 +10,12 @@ pub mod git {
use crate::fs::fields as f;
pub struct GitCache;
impl FromIterator<PathBuf> for GitCache {
fn from_iter<I>(_iter: I) -> Self
where I: IntoIterator<Item=PathBuf>
where
I: IntoIterator<Item = PathBuf>,
{
Self
}
@ -31,8 +31,8 @@ pub mod git {
}
}
impl f::SubdirGitRepo{
pub fn from_path(_dir : &Path, _status : bool) -> Self{
impl f::SubdirGitRepo {
pub fn from_path(_dir: &Path, _status: bool) -> Self {
panic!("Tried to get subdir Git status, but Git support is disabled")
}
}

View File

@ -1,6 +1,6 @@
//! Extended attribute support for Darwin and Linux systems.
#![allow(trivial_casts)] // for ARM
#![allow(trivial_casts)] // for ARM
#[cfg(any(target_os = "macos", target_os = "linux"))]
use std::cmp::Ordering;
@ -9,10 +9,8 @@ use std::ffi::CString;
use std::io;
use std::path::Path;
pub const ENABLED: bool = cfg!(any(target_os = "macos", target_os = "linux"));
pub trait FileAttributes {
fn attributes(&self) -> io::Result<Vec<Attribute>>;
fn symlink_attributes(&self) -> io::Result<Vec<Attribute>>;
@ -40,7 +38,6 @@ impl FileAttributes for Path {
}
}
/// Attributes which can be passed to `Attribute::list_with_flags`
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[derive(Copy, Clone)]
@ -56,15 +53,13 @@ pub struct Attribute {
pub value: String,
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn get_secattr(lister: &lister::Lister, c_path: &std::ffi::CString) -> io::Result<Vec<Attribute>> {
const SELINUX_XATTR_NAME: &str = "security.selinux";
const ENODATA: i32 = 61;
let c_attr_name = CString::new(SELINUX_XATTR_NAME).map_err(|e| {
io::Error::new(io::ErrorKind::Other, e)
})?;
let c_attr_name =
CString::new(SELINUX_XATTR_NAME).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let size = lister.getxattr_first(c_path, &c_attr_name);
let size = match size.cmp(&0) {
@ -72,11 +67,11 @@ fn get_secattr(lister: &lister::Lister, c_path: &std::ffi::CString) -> io::Resul
let e = io::Error::last_os_error();
if e.kind() == io::ErrorKind::Other && e.raw_os_error() == Some(ENODATA) {
return Ok(Vec::new())
return Ok(Vec::new());
}
return Err(e)
},
return Err(e);
}
Ordering::Equal => return Err(io::Error::from(io::ErrorKind::InvalidData)),
Ordering::Greater => size as usize,
};
@ -91,32 +86,34 @@ fn get_secattr(lister: &lister::Lister, c_path: &std::ffi::CString) -> io::Resul
}
Ok(vec![Attribute {
name: String::from(SELINUX_XATTR_NAME),
name: String::from(SELINUX_XATTR_NAME),
value: lister.translate_attribute_data(&buf_value),
}])
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attribute>> {
let c_path = CString::new(path.to_str().ok_or(io::Error::new(io::ErrorKind::Other, "Error: path not convertible to string"))?).map_err(|e| {
io::Error::new(io::ErrorKind::Other, e)
})?;
let c_path = CString::new(path.to_str().ok_or(io::Error::new(
io::ErrorKind::Other,
"Error: path not convertible to string",
))?)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let bufsize = lister.listxattr_first(&c_path);
let bufsize = match bufsize.cmp(&0) {
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Less => return Err(io::Error::last_os_error()),
// Some filesystems, like sysfs, return nothing on listxattr, even though the security
// attribute is set.
Ordering::Equal => return get_secattr(lister, &c_path),
Ordering::Greater => bufsize as usize,
Ordering::Equal => return get_secattr(lister, &c_path),
Ordering::Greater => bufsize as usize,
};
let mut buf = vec![0_u8; bufsize];
match lister.listxattr_second(&c_path, &mut buf, bufsize).cmp(&0) {
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Equal => return Ok(Vec::new()),
Ordering::Greater => {},
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Equal => return Ok(Vec::new()),
Ordering::Greater => {}
}
let mut names = Vec::new();
@ -126,9 +123,8 @@ pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attrib
continue;
}
let c_attr_name = CString::new(attr_name).map_err(|e| {
io::Error::new(io::ErrorKind::Other, e)
})?;
let c_attr_name =
CString::new(attr_name).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let size = lister.getxattr_first(&c_path, &c_attr_name);
if size > 0 {
@ -138,12 +134,12 @@ pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attrib
}
names.push(Attribute {
name: lister.translate_attribute_data(attr_name),
name: lister.translate_attribute_data(attr_name),
value: lister.translate_attribute_data(&buf_value),
});
} else {
names.push(Attribute {
name: lister.translate_attribute_data(attr_name),
name: lister.translate_attribute_data(attr_name),
value: String::new(),
});
}
@ -152,11 +148,10 @@ pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attrib
Ok(names)
}
#[cfg(target_os = "macos")]
mod lister {
use super::FollowSymlinks;
use libc::{c_int, size_t, ssize_t, c_char, c_void};
use libc::{c_char, c_int, c_void, size_t, ssize_t};
use std::ffi::CString;
use std::ptr;
@ -185,29 +180,31 @@ mod lister {
impl Lister {
pub fn new(do_follow: FollowSymlinks) -> Self {
let c_flags: c_int = match do_follow {
FollowSymlinks::Yes => 0x0001,
FollowSymlinks::No => 0x0000,
FollowSymlinks::Yes => 0x0001,
FollowSymlinks::No => 0x0000,
};
Self { c_flags }
}
pub fn translate_attribute_data(&self, input: &[u8]) -> String {
unsafe { std::str::from_utf8_unchecked(input).trim_end_matches('\0').into() }
}
pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
unsafe {
listxattr(
c_path.as_ptr(),
ptr::null_mut(),
0,
self.c_flags,
)
std::str::from_utf8_unchecked(input)
.trim_end_matches('\0')
.into()
}
}
pub fn listxattr_second(&self, c_path: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
unsafe { listxattr(c_path.as_ptr(), ptr::null_mut(), 0, self.c_flags) }
}
pub fn listxattr_second(
&self,
c_path: &CString,
buf: &mut [u8],
bufsize: size_t,
) -> ssize_t {
unsafe {
listxattr(
c_path.as_ptr(),
@ -231,7 +228,13 @@ mod lister {
}
}
pub fn getxattr_second(&self, c_path: &CString, c_name: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
pub fn getxattr_second(
&self,
c_path: &CString,
c_name: &CString,
buf: &mut [u8],
bufsize: size_t,
) -> ssize_t {
unsafe {
getxattr(
c_path.as_ptr(),
@ -246,26 +249,17 @@ mod lister {
}
}
#[cfg(target_os = "linux")]
mod lister {
use std::ffi::CString;
use libc::{size_t, ssize_t, c_char, c_void};
use super::FollowSymlinks;
use libc::{c_char, c_void, size_t, ssize_t};
use std::ffi::CString;
use std::ptr;
extern "C" {
fn listxattr(
path: *const c_char,
list: *mut c_char,
size: size_t,
) -> ssize_t;
fn listxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t;
fn llistxattr(
path: *const c_char,
list: *mut c_char,
size: size_t,
) -> ssize_t;
fn llistxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t;
fn getxattr(
path: *const c_char,
@ -297,54 +291,46 @@ mod lister {
pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
let listxattr = match self.follow_symlinks {
FollowSymlinks::Yes => listxattr,
FollowSymlinks::No => llistxattr,
FollowSymlinks::Yes => listxattr,
FollowSymlinks::No => llistxattr,
};
unsafe {
listxattr(
c_path.as_ptr(),
ptr::null_mut(),
0,
)
}
unsafe { listxattr(c_path.as_ptr(), ptr::null_mut(), 0) }
}
pub fn listxattr_second(&self, c_path: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
pub fn listxattr_second(
&self,
c_path: &CString,
buf: &mut [u8],
bufsize: size_t,
) -> ssize_t {
let listxattr = match self.follow_symlinks {
FollowSymlinks::Yes => listxattr,
FollowSymlinks::No => llistxattr,
FollowSymlinks::Yes => listxattr,
FollowSymlinks::No => llistxattr,
};
unsafe {
listxattr(
c_path.as_ptr(),
buf.as_mut_ptr().cast(),
bufsize,
)
}
unsafe { listxattr(c_path.as_ptr(), buf.as_mut_ptr().cast(), bufsize) }
}
pub fn getxattr_first(&self, c_path: &CString, c_name: &CString) -> ssize_t {
let getxattr = match self.follow_symlinks {
FollowSymlinks::Yes => getxattr,
FollowSymlinks::No => lgetxattr,
FollowSymlinks::No => lgetxattr,
};
unsafe {
getxattr(
c_path.as_ptr(),
c_name.as_ptr().cast(),
ptr::null_mut(),
0,
)
}
unsafe { getxattr(c_path.as_ptr(), c_name.as_ptr().cast(), ptr::null_mut(), 0) }
}
pub fn getxattr_second(&self, c_path: &CString, c_name: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
pub fn getxattr_second(
&self,
c_path: &CString,
c_name: &CString,
buf: &mut [u8],
bufsize: size_t,
) -> ssize_t {
let getxattr = match self.follow_symlinks {
FollowSymlinks::Yes => getxattr,
FollowSymlinks::No => lgetxattr,
FollowSymlinks::No => lgetxattr,
};
unsafe {

View File

@ -29,7 +29,6 @@ pub type time_t = i64;
/// The type of a files user ID.
pub type uid_t = u32;
/// The files base type, which gets displayed in the very first column of the
/// details output.
///
@ -56,9 +55,9 @@ impl Type {
}
}
/// The files Unix permission bitfield, with one entry per bit.
#[derive(Copy, Clone)]
#[rustfmt::skip]
pub struct Permissions {
pub user_read: bool,
pub user_write: bool,
@ -79,6 +78,7 @@ pub struct Permissions {
/// The file's `FileAttributes` field, available only on Windows.
#[derive(Copy, Clone)]
#[rustfmt::skip]
pub struct Attributes {
pub archive: bool,
pub directory: bool,
@ -93,15 +93,14 @@ pub struct Attributes {
/// little more compressed.
#[derive(Copy, Clone)]
pub struct PermissionsPlus {
pub file_type: Type,
pub file_type: Type,
#[cfg(unix)]
pub permissions: Permissions,
#[cfg(windows)]
pub attributes: Attributes,
pub xattrs: bool,
pub attributes: Attributes,
pub xattrs: bool,
}
/// The permissions encoded as octal values
#[derive(Copy, Clone)]
pub struct OctalPermissions {
@ -116,7 +115,6 @@ pub struct OctalPermissions {
/// block count specifically for this case.
#[derive(Copy, Clone)]
pub struct Links {
/// The actual link count.
pub count: nlink_t,
@ -124,19 +122,16 @@ pub struct Links {
pub multiple: bool,
}
/// A files inode. Every directory entry on a Unix filesystem has an inode,
/// including directories and links, so this is applicable to everything exa
/// can deal with.
#[derive(Copy, Clone)]
pub struct Inode(pub ino_t);
/// A file's size of allocated file system blocks.
#[derive(Copy, Clone)]
#[cfg(unix)]
pub enum Blocksize {
/// This file has the given number of blocks.
Some(u64),
@ -144,7 +139,6 @@ pub enum Blocksize {
None,
}
/// The ID of the user that owns a file. This will only ever be a number;
/// looking up the username is done in the `display` module.
#[derive(Copy, Clone)]
@ -154,12 +148,10 @@ pub struct User(pub uid_t);
#[derive(Copy, Clone)]
pub struct Group(pub gid_t);
/// A files size, in bytes. This is usually formatted by the `number_prefix`
/// crate into something human-readable.
#[derive(Copy, Clone)]
pub enum Size {
/// This file has a defined size.
Some(u64),
@ -194,7 +186,6 @@ pub struct DeviceIDs {
pub minor: u32,
}
/// One of a files timestamps (created, accessed, or modified).
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Time {
@ -202,13 +193,11 @@ pub struct Time {
pub nanoseconds: time_t,
}
/// A files status in a Git repository. Whether a file is in a repository or
/// not is handled by the Git module, rather than having a “null” variant in
/// this enum.
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum GitStatus {
/// This file hasnt changed since the last commit.
NotModified,
@ -235,18 +224,16 @@ pub enum GitStatus {
Conflicted,
}
/// A files complete Git status. Its possible to make changes to a file, add
/// it to the staging area, then make *more* changes, so we need to list each
/// files status for both of these.
#[derive(Copy, Clone)]
pub struct Git {
pub staged: GitStatus,
pub staged: GitStatus,
pub unstaged: GitStatus,
}
impl Default for Git {
/// Create a Git status for a file with nothing done to it.
fn default() -> Self {
Self {
@ -258,7 +245,7 @@ impl Default for Git {
pub enum SecurityContextType<'a> {
SELinux(&'a str),
None
None,
}
pub struct SecurityContext<'a> {
@ -267,7 +254,7 @@ pub struct SecurityContext<'a> {
#[allow(dead_code)]
#[derive(PartialEq, Copy, Clone)]
pub enum SubdirGitRepoStatus{
pub enum SubdirGitRepoStatus {
NoRepo,
GitClean,
GitDirty,
@ -279,7 +266,7 @@ pub struct SubdirGitRepo{
pub branch : Option<String>
}
impl Default for SubdirGitRepo{
impl Default for SubdirGitRepo {
fn default() -> Self {
Self{
status: Some(SubdirGitRepoStatus::NoRepo),

View File

@ -14,13 +14,12 @@ use log::*;
use crate::fs::dir::Dir;
use crate::fs::feature::xattr;
use crate::fs::feature::xattr::{FileAttributes, Attribute};
use crate::fs::feature::xattr::{Attribute, FileAttributes};
use crate::fs::fields as f;
use super::mounts::all_mounts;
use super::mounts::MountedFs;
/// A **File** is a wrapper around one of Rusts `PathBuf` values, along with
/// associated data about the file.
///
@ -29,7 +28,6 @@ use super::mounts::MountedFs;
/// information queried at least once, so it makes sense to do all this at the
/// start and hold on to all the information.
pub struct File<'dir> {
/// The filename portion of this files path, including the extension.
///
/// This is used to compare against certain filenames (such as checking if
@ -89,48 +87,84 @@ pub struct File<'dir> {
}
impl<'dir> File<'dir> {
pub fn from_args<PD, FN>(path: PathBuf, parent_dir: PD, filename: FN, deref_links: bool) -> io::Result<File<'dir>>
where PD: Into<Option<&'dir Dir>>,
FN: Into<Option<String>>
pub fn from_args<PD, FN>(
path: PathBuf,
parent_dir: PD,
filename: FN,
deref_links: bool,
) -> io::Result<File<'dir>>
where
PD: Into<Option<&'dir Dir>>,
FN: Into<Option<String>>,
{
let parent_dir = parent_dir.into();
let name = filename.into().unwrap_or_else(|| File::filename(&path));
let ext = File::ext(&path);
let name = filename.into().unwrap_or_else(|| File::filename(&path));
let ext = File::ext(&path);
debug!("Statting file {:?}", &path);
let metadata = std::fs::symlink_metadata(&path)?;
let metadata = std::fs::symlink_metadata(&path)?;
let is_all_all = false;
let extended_attributes = OnceLock::new();
let absolute_path = OnceLock::new();
Ok(File { name, ext, path, metadata, parent_dir, is_all_all, deref_links, extended_attributes, absolute_path })
Ok(File {
name,
ext,
path,
metadata,
parent_dir,
is_all_all,
deref_links,
extended_attributes,
absolute_path,
})
}
pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
let path = parent_dir.path.clone();
let ext = File::ext(&path);
let path = parent_dir.path.clone();
let ext = File::ext(&path);
debug!("Statting file {:?}", &path);
let metadata = std::fs::symlink_metadata(&path)?;
let metadata = std::fs::symlink_metadata(&path)?;
let is_all_all = true;
let parent_dir = Some(parent_dir);
let extended_attributes = OnceLock::new();
let absolute_path = OnceLock::new();
Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, deref_links: false, extended_attributes, absolute_path })
Ok(File {
path,
parent_dir,
metadata,
ext,
name: ".".into(),
is_all_all,
deref_links: false,
extended_attributes,
absolute_path,
})
}
pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
let ext = File::ext(&path);
let ext = File::ext(&path);
debug!("Statting file {:?}", &path);
let metadata = std::fs::symlink_metadata(&path)?;
let metadata = std::fs::symlink_metadata(&path)?;
let is_all_all = true;
let parent_dir = Some(parent_dir);
let extended_attributes = OnceLock::new();
let absolute_path = OnceLock::new();
Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, deref_links: false, extended_attributes, absolute_path })
Ok(File {
path,
parent_dir,
metadata,
ext,
name: "..".into(),
is_all_all,
deref_links: false,
extended_attributes,
absolute_path,
})
}
/// A files name is derived from its string. This needs to handle directories
@ -139,8 +173,7 @@ impl<'dir> File<'dir> {
pub fn filename(path: &Path) -> String {
if let Some(back) = path.components().next_back() {
back.as_os_str().to_string_lossy().to_string()
}
else {
} else {
// use the path as fallback
error!("Path {:?} has no last component", path);
path.display().to_string()
@ -158,9 +191,7 @@ impl<'dir> File<'dir> {
fn ext(path: &Path) -> Option<String> {
let name = path.file_name().map(|f| f.to_string_lossy().to_string())?;
name.rfind('.')
.map(|p| name[p + 1 ..]
.to_ascii_lowercase())
name.rfind('.').map(|p| name[p + 1..].to_ascii_lowercase())
}
/// Read the extended attributes of a file path.
@ -169,7 +200,11 @@ impl<'dir> File<'dir> {
match path.symlink_attributes() {
Ok(xattrs) => xattrs,
Err(e) => {
error!("Error looking up extended attributes for {}: {}", path.display(), e);
error!(
"Error looking up extended attributes for {}: {}",
path.display(),
e
);
Vec::new()
}
}
@ -180,7 +215,8 @@ impl<'dir> File<'dir> {
/// Get the extended attributes of a file path on demand.
pub fn extended_attributes(&self) -> &Vec<Attribute> {
self.extended_attributes.get_or_init(||File::gather_extended_attributes(&self.path))
self.extended_attributes
.get_or_init(|| File::gather_extended_attributes(&self.path))
}
/// Whether this file is a directory on the filesystem.
@ -261,19 +297,23 @@ impl<'dir> File<'dir> {
/// Determine the full path resolving all symbolic links on demand.
pub fn absolute_path(&self) -> Option<&PathBuf> {
self.absolute_path.get_or_init(|| std::fs::canonicalize(&self.path).ok()).as_ref()
self.absolute_path
.get_or_init(|| std::fs::canonicalize(&self.path).ok())
.as_ref()
}
/// Whether this file is a mount point
pub fn is_mount_point(&self) -> bool {
cfg!(any(target_os = "linux", target_os = "macos")) &&
self.is_directory() &&
self.absolute_path().is_some_and(|p| all_mounts().contains_key(p))
cfg!(any(target_os = "linux", target_os = "macos"))
&& self.is_directory()
&& self
.absolute_path()
.is_some_and(|p| all_mounts().contains_key(p))
}
/// The filesystem device and type for a mount point
pub fn mount_point_info(&self) -> Option<&MountedFs> {
if cfg!(any(target_os = "linux",target_os = "macos")) {
if cfg!(any(target_os = "linux", target_os = "macos")) {
return self.absolute_path().and_then(|p| all_mounts().get(p));
}
None
@ -285,14 +325,11 @@ impl<'dir> File<'dir> {
fn reorient_target_path(&self, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
}
else if let Some(dir) = self.parent_dir {
} else if let Some(dir) = self.parent_dir {
dir.join(path)
}
else if let Some(parent) = self.path.parent() {
} else if let Some(parent) = self.path.parent() {
parent.join(path)
}
else {
} else {
self.path.join(path)
}
}
@ -308,15 +345,14 @@ impl<'dir> File<'dir> {
/// existed. If this file cannot be read at all, returns the error that
/// we got when we tried to read it.
pub fn link_target(&self) -> FileTarget<'dir> {
// We need to be careful to treat the path actually pointed to by
// this file — which could be absolute or relative — to the path
// we actually look up and turn into a `File` — which needs to be
// absolute to be accessible from any directory.
debug!("Reading link {:?}", &self.path);
let path = match std::fs::read_link(&self.path) {
Ok(p) => p,
Err(e) => return FileTarget::Err(e),
Ok(p) => p,
Err(e) => return FileTarget::Err(e),
};
let absolute_path = self.reorient_target_path(&path);
@ -325,7 +361,7 @@ impl<'dir> File<'dir> {
// follow links.
match std::fs::metadata(&absolute_path) {
Ok(metadata) => {
let ext = File::ext(&path);
let ext = File::ext(&path);
let name = File::filename(&path);
let extended_attributes = OnceLock::new();
let absolute_path_cell = OnceLock::from(Some(absolute_path));
@ -401,8 +437,7 @@ impl<'dir> File<'dir> {
// for 512 byte blocks according to the POSIX standard
// even though the physical block size may be different.
f::Blocksize::Some(self.metadata.blocks() * 512)
}
else {
} else {
f::Blocksize::None
}
}
@ -413,8 +448,8 @@ impl<'dir> File<'dir> {
pub fn user(&self) -> Option<f::User> {
if self.is_link() && self.deref_links {
match self.link_target_recurse() {
FileTarget::Ok(f) => return f.user(),
_ => return None,
FileTarget::Ok(f) => return f.user(),
_ => return None,
}
}
Some(f::User(self.metadata.uid()))
@ -425,8 +460,8 @@ impl<'dir> File<'dir> {
pub fn group(&self) -> Option<f::Group> {
if self.is_link() && self.deref_links {
match self.link_target_recurse() {
FileTarget::Ok(f) => return f.group(),
_ => return None,
FileTarget::Ok(f) => return f.group(),
_ => return None,
}
}
Some(f::Group(self.metadata.gid()))
@ -454,8 +489,7 @@ impl<'dir> File<'dir> {
}
if self.is_directory() {
f::Size::None
}
else if self.is_char_device() || self.is_block_device() {
} else if self.is_char_device() || self.is_block_device() {
let device_id = self.metadata.rdev();
// MacOS and Linux have different arguments and return types for the
@ -470,11 +504,10 @@ impl<'dir> File<'dir> {
major: unsafe { libc::major(device_id.try_into().unwrap()) } as u32,
minor: unsafe { libc::minor(device_id.try_into().unwrap()) } as u32,
})
}
else if self.is_link() && self.deref_links {
} else if self.is_link() && self.deref_links {
match self.link_target() {
FileTarget::Ok(f) => f.size(),
_ => f::Size::None
_ => f::Size::None,
}
} else {
f::Size::Some(self.metadata.len())
@ -483,21 +516,20 @@ impl<'dir> File<'dir> {
/// Returns the size of the file or indicates no size if it's a directory.
///
/// For Windows platforms, the size of directories is not computed and will
/// For Windows platforms, the size of directories is not computed and will
/// return `Size::None`.
#[cfg(windows)]
pub fn size(&self) -> f::Size {
if self.is_directory() {
f::Size::None
}
else {
} else {
f::Size::Some(self.metadata.len())
}
}
/// Determines if the directory is empty or not.
///
/// For Unix platforms, this function first checks the link count to quickly
/// For Unix platforms, this function first checks the link count to quickly
/// determine non-empty directories. On most UNIX filesystems the link count
/// is two plus the number of subdirectories. If the link count is less than
/// or equal to 2, it then checks the directory contents to determine if
@ -524,8 +556,8 @@ impl<'dir> File<'dir> {
/// Determines if the directory is empty or not.
///
/// For Windows platforms, this function checks the directory contents directly
/// to determine if it's empty. Since certain filesystems on Windows make it
/// For Windows platforms, this function checks the directory contents directly
/// to determine if it's empty. Since certain filesystems on Windows make it
/// challenging to infer emptiness based on directory size, this approach is used.
#[cfg(windows)]
pub fn is_empty_dir(&self) -> bool {
@ -538,7 +570,7 @@ impl<'dir> File<'dir> {
/// Checks the contents of the directory to determine if it's empty.
///
/// This function avoids counting '.' and '..' when determining if the directory is
/// This function avoids counting '.' and '..' when determining if the directory is
/// empty. If any other entries are found, it returns `false`.
///
/// The naive approach, as one would think that this info may have been cached.
@ -548,7 +580,10 @@ impl<'dir> File<'dir> {
trace!("is_empty_directory: reading dir");
match Dir::read_dir(self.path.clone()) {
// . & .. are skipped, if the returned iterator has .next(), it's not empty
Ok(has_files) => has_files.files(super::DotFilter::Dotfiles, None, false, false).next().is_none(),
Ok(has_files) => has_files
.files(super::DotFilter::Dotfiles, None, false, false)
.next()
.is_none(),
Err(_) => false,
}
}
@ -558,10 +593,13 @@ impl<'dir> File<'dir> {
if self.is_link() && self.deref_links {
return match self.link_target_recurse() {
FileTarget::Ok(f) => f.modified_time(),
_ => None,
_ => None,
};
}
self.metadata.modified().map(|st| DateTime::<Utc>::from(st).naive_utc()).ok()
self.metadata
.modified()
.map(|st| DateTime::<Utc>::from(st).naive_utc())
.ok()
}
/// This files last changed timestamp, if available on this platform.
@ -573,10 +611,7 @@ impl<'dir> File<'dir> {
_ => None,
};
}
NaiveDateTime::from_timestamp_opt(
self.metadata.ctime(),
self.metadata.ctime_nsec() as u32,
)
NaiveDateTime::from_timestamp_opt(self.metadata.ctime(), self.metadata.ctime_nsec() as u32)
}
#[cfg(windows)]
@ -589,10 +624,13 @@ impl<'dir> File<'dir> {
if self.is_link() && self.deref_links {
return match self.link_target_recurse() {
FileTarget::Ok(f) => f.accessed_time(),
_ => None,
_ => None,
};
}
self.metadata.accessed().map(|st| DateTime::<Utc>::from(st).naive_utc()).ok()
self.metadata
.accessed()
.map(|st| DateTime::<Utc>::from(st).naive_utc())
.ok()
}
/// This files created timestamp, if available on this platform.
@ -603,7 +641,10 @@ impl<'dir> File<'dir> {
_ => None,
};
}
self.metadata.created().map(|st| DateTime::<Utc>::from(st).naive_utc()).ok()
self.metadata
.created()
.map(|st| DateTime::<Utc>::from(st).naive_utc())
.ok()
}
/// This files type.
@ -615,26 +656,19 @@ impl<'dir> File<'dir> {
pub fn type_char(&self) -> f::Type {
if self.is_file() {
f::Type::File
}
else if self.is_directory() {
} else if self.is_directory() {
f::Type::Directory
}
else if self.is_pipe() {
} else if self.is_pipe() {
f::Type::Pipe
}
else if self.is_link() {
} else if self.is_link() {
f::Type::Link
}
else if self.is_char_device() {
} else if self.is_char_device() {
f::Type::CharDevice
}
else if self.is_block_device() {
} else if self.is_block_device() {
f::Type::BlockDevice
}
else if self.is_socket() {
} else if self.is_socket() {
f::Type::Socket
}
else {
} else {
f::Type::Special
}
}
@ -643,11 +677,9 @@ impl<'dir> File<'dir> {
pub fn type_char(&self) -> f::Type {
if self.is_file() {
f::Type::File
}
else if self.is_directory() {
} else if self.is_directory() {
f::Type::Directory
}
else {
} else {
f::Type::Special
}
}
@ -660,29 +692,29 @@ impl<'dir> File<'dir> {
// return the permissions of the original link, as would have been
// done if we were not dereferencing.
match self.link_target_recurse() {
FileTarget::Ok(f) => return f.permissions(),
_ => return None,
FileTarget::Ok(f) => return f.permissions(),
_ => return None,
}
}
let bits = self.metadata.mode();
let has_bit = |bit| bits & bit == bit;
Some(f::Permissions {
user_read: has_bit(modes::USER_READ),
user_write: has_bit(modes::USER_WRITE),
user_execute: has_bit(modes::USER_EXECUTE),
user_read: has_bit(modes::USER_READ),
user_write: has_bit(modes::USER_WRITE),
user_execute: has_bit(modes::USER_EXECUTE),
group_read: has_bit(modes::GROUP_READ),
group_write: has_bit(modes::GROUP_WRITE),
group_execute: has_bit(modes::GROUP_EXECUTE),
group_read: has_bit(modes::GROUP_READ),
group_write: has_bit(modes::GROUP_WRITE),
group_execute: has_bit(modes::GROUP_EXECUTE),
other_read: has_bit(modes::OTHER_READ),
other_write: has_bit(modes::OTHER_WRITE),
other_execute: has_bit(modes::OTHER_EXECUTE),
other_read: has_bit(modes::OTHER_READ),
other_write: has_bit(modes::OTHER_WRITE),
other_execute: has_bit(modes::OTHER_EXECUTE),
sticky: has_bit(modes::STICKY),
setgid: has_bit(modes::SETGID),
setuid: has_bit(modes::SETUID),
sticky: has_bit(modes::STICKY),
setgid: has_bit(modes::SETGID),
setuid: has_bit(modes::SETUID),
})
}
@ -693,37 +725,38 @@ impl<'dir> File<'dir> {
// https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
f::Attributes {
directory: has_bit(0x10),
archive: has_bit(0x20),
readonly: has_bit(0x1),
hidden: has_bit(0x2),
system: has_bit(0x4),
reparse_point: has_bit(0x400),
directory: has_bit(0x10),
archive: has_bit(0x20),
readonly: has_bit(0x1),
hidden: has_bit(0x2),
system: has_bit(0x4),
reparse_point: has_bit(0x400),
}
}
/// This files security context field.
pub fn security_context(&self) -> f::SecurityContext<'_> {
let context = match self.extended_attributes().iter().find(|a| a.name == "security.selinux") {
let context = match self
.extended_attributes()
.iter()
.find(|a| a.name == "security.selinux")
{
Some(attr) => f::SecurityContextType::SELinux(&attr.value),
None => f::SecurityContextType::None
None => f::SecurityContextType::None,
};
f::SecurityContext { context }
}
}
impl<'a> AsRef<File<'a>> for File<'a> {
fn as_ref(&self) -> &File<'a> {
self
}
}
/// The result of following a symlink.
pub enum FileTarget<'dir> {
/// The symlink pointed at a file that exists.
Ok(Box<File<'dir>>),
@ -735,14 +768,12 @@ pub enum FileTarget<'dir> {
/// file isnt a link to begin with, but also if, say, we dont have
/// permission to follow it.
Err(io::Error),
// Err is its own variant, instead of having the whole thing be inside an
// `io::Result`, because being unable to follow a symlink is not a serious
// error — we just display the error message and move on.
}
impl<'dir> FileTarget<'dir> {
/// Whether this link doesnt 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 {
@ -750,7 +781,6 @@ impl<'dir> FileTarget<'dir> {
}
}
/// More readable aliases for the permission bits exposed by libc.
#[allow(trivial_numeric_casts)]
#[cfg(unix)]
@ -760,24 +790,23 @@ mod modes {
// from `metadata.permissions().mode()` is always `u32`.
pub type Mode = u32;
pub const USER_READ: Mode = libc::S_IRUSR as Mode;
pub const USER_WRITE: Mode = libc::S_IWUSR as Mode;
pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode;
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_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_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;
pub const STICKY: Mode = libc::S_ISVTX as Mode;
pub const SETGID: Mode = libc::S_ISGID as Mode;
pub const SETUID: Mode = libc::S_ISUID as Mode;
pub const STICKY: Mode = libc::S_ISVTX as Mode;
pub const SETGID: Mode = libc::S_ISGID as Mode;
pub const SETUID: Mode = libc::S_ISUID as Mode;
}
#[cfg(test)]
mod ext_test {
use super::File;
@ -799,7 +828,6 @@ mod ext_test {
}
}
#[cfg(test)]
mod filename_test {
use super::File;

View File

@ -9,7 +9,6 @@ use crate::fs::DotFilter;
use crate::fs::File;
/// Flags used to manage the **file filter** process
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum FileFilterFlags {
@ -25,7 +24,6 @@ pub enum FileFilterFlags {
OnlyFiles
}
/// The **file filter** processes a list of files before displaying them to
/// the user, by removing files they dont want to see, and putting the list
/// in the desired order.
@ -42,7 +40,6 @@ pub enum FileFilterFlags {
/// performing the comparison.
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct FileFilter {
/// Whether directories should be listed first, and other types of file
/// second. Some users prefer it like this.
pub list_dirs_first: bool,
@ -79,8 +76,8 @@ impl FileFilter {
/// filter predicate for files found inside a directory.
pub fn filter_child_files(&self, files: &mut Vec<File<'_>>) {
use FileFilterFlags::{OnlyDirs, OnlyFiles};
files.retain(|f| ! self.ignore_patterns.is_ignored(&f.name));
files.retain(|f| !self.ignore_patterns.is_ignored(&f.name));
match (self.flags.contains(&OnlyDirs), self.flags.contains(&OnlyFiles)) {
(true, false) => {
@ -93,7 +90,6 @@ impl FileFilter {
}
_ => {}
}
}
/// Remove every file in the given vector that does *not* pass the
@ -106,18 +102,15 @@ impl FileFilter {
/// `exa -I='*.ogg' music/*` should filter out the ogg files obtained
/// from the glob, even though the globbing is done by the shell!
pub fn filter_argument_files(&self, files: &mut Vec<File<'_>>) {
files.retain(|f| {
! self.ignore_patterns.is_ignored(&f.name)
});
files.retain(|f| !self.ignore_patterns.is_ignored(&f.name));
}
/// Sort the files in the given vector based on the sort field option.
pub fn sort_files<'a, F>(&self, files: &mut [F])
where F: AsRef<File<'a>>
where
F: AsRef<File<'a>>,
{
files.sort_by(|a, b| {
self.sort_field.compare_files(a.as_ref(), b.as_ref())
});
files.sort_by(|a, b| self.sort_field.compare_files(a.as_ref(), b.as_ref()));
if self.flags.contains(&FileFilterFlags::Reverse) {
files.reverse();
@ -127,18 +120,17 @@ impl FileFilter {
// This relies on the fact that `sort_by` is *stable*: it will keep
// adjacent elements next to each other.
files.sort_by(|a, b| {
b.as_ref().points_to_directory()
b.as_ref()
.points_to_directory()
.cmp(&a.as_ref().points_to_directory())
});
}
}
}
/// User-supplied field to sort by.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum SortField {
/// Dont apply any sorting. This is usually used as an optimisation in
/// scripts, where the order doesnt matter.
Unsorted,
@ -219,7 +211,6 @@ pub enum SortField {
/// effects they have.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum SortCase {
/// Sort files case-sensitively with uppercase first, with A coming
/// before a.
ABCabc,
@ -229,7 +220,6 @@ pub enum SortCase {
}
impl SortField {
/// Compares two files to determine the order they should be listed in,
/// depending on the search field.
///
@ -241,6 +231,7 @@ impl SortField {
pub fn compare_files(self, a: &File<'_>, b: &File<'_>) -> Ordering {
use self::SortCase::{ABCabc, AaBbCc};
#[rustfmt::skip]
match self {
Self::Unsorted => Ordering::Equal,
@ -285,12 +276,11 @@ impl SortField {
fn strip_dot(n: &str) -> &str {
match n.strip_prefix('.') {
Some(s) => s,
None => n,
None => n,
}
}
}
/// The **ignore patterns** are a list of globs that are tested against
/// each filename, and if any of them match, that file isnt displayed.
/// This lets a user hide, say, text files by ignoring `*.txt`.
@ -300,9 +290,9 @@ pub struct IgnorePatterns {
}
impl FromIterator<glob::Pattern> for IgnorePatterns {
fn from_iter<I>(iter: I) -> Self
where I: IntoIterator<Item = glob::Pattern>
where
I: IntoIterator<Item = glob::Pattern>,
{
let patterns = iter.into_iter().collect();
Self { patterns }
@ -310,18 +300,19 @@ impl FromIterator<glob::Pattern> for IgnorePatterns {
}
impl IgnorePatterns {
/// Create a new list from the input glob strings, turning the inputs that
/// are valid glob patterns into an `IgnorePatterns`. The inputs that
/// dont parse correctly are returned separately.
pub fn parse_from_iter<'a, I: IntoIterator<Item = &'a str>>(iter: I) -> (Self, Vec<glob::PatternError>) {
pub fn parse_from_iter<'a, I: IntoIterator<Item = &'a str>>(
iter: I,
) -> (Self, Vec<glob::PatternError>) {
let iter = iter.into_iter();
// Almost all glob patterns are valid, so its worth pre-allocating
// the vector with enough space for all of them.
let mut patterns = match iter.size_hint() {
(_, Some(count)) => Vec::with_capacity(count),
_ => Vec::new(),
(_, Some(count)) => Vec::with_capacity(count),
_ => Vec::new(),
};
// Similarly, assume there wont be any errors.
@ -330,7 +321,7 @@ impl IgnorePatterns {
for input in iter {
match glob::Pattern::new(input) {
Ok(pat) => patterns.push(pat),
Err(e) => errors.push(e),
Err(e) => errors.push(e),
}
}
@ -339,7 +330,9 @@ impl IgnorePatterns {
/// Create a new empty set of patterns that matches nothing.
pub fn empty() -> Self {
Self { patterns: Vec::new() }
Self {
patterns: Vec::new(),
}
}
/// Test whether the given file should be hidden from the results.
@ -348,11 +341,9 @@ impl IgnorePatterns {
}
}
/// Whether to ignore or display files that Git would ignore.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum GitIgnore {
/// Ignore files that Git would ignore.
CheckAndIgnore,
@ -360,8 +351,6 @@ pub enum GitIgnore {
Off,
}
#[cfg(test)]
mod test_ignores {
use super::*;
@ -375,7 +364,7 @@ mod test_ignores {
#[test]
fn ignores_a_glob() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "*.mp3" ]);
let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["*.mp3"]);
assert!(fails.is_empty());
assert!(!pats.is_ignored("nothing"));
assert!(pats.is_ignored("test.mp3"));
@ -383,7 +372,7 @@ mod test_ignores {
#[test]
fn ignores_an_exact_filename() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing" ]);
let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["nothing"]);
assert!(fails.is_empty());
assert!(pats.is_ignored("nothing"));
assert!(!pats.is_ignored("test.mp3"));
@ -391,7 +380,7 @@ mod test_ignores {
#[test]
fn ignores_both() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing", "*.mp3" ]);
let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["nothing", "*.mp3"]);
assert!(fails.is_empty());
assert!(pats.is_ignored("nothing"));
assert!(pats.is_ignored("test.mp3"));

View File

@ -1,16 +1,16 @@
use proc_mounts::MountList;
use crate::fs::mounts::{Error, MountedFs};
use proc_mounts::MountList;
/// Get a list of all mounted filesystems
pub fn mounts() -> Result<Vec<MountedFs>, Error> {
Ok(MountList::new()
.map_err(Error::IOError)?
.0.iter()
.map(|mount| MountedFs {
dest: mount.dest.clone(),
fstype: mount.fstype.clone(),
source: mount.source.to_string_lossy().into()
})
.collect()
)
}
.map_err(Error::IOError)?
.0
.iter()
.map(|mount| MountedFs {
dest: mount.dest.clone(),
fstype: mount.fstype.clone(),
source: mount.source.to_string_lossy().into(),
})
.collect())
}

View File

@ -1,10 +1,10 @@
use std::{mem, ptr};
use crate::fs::mounts::{Error, MountedFs};
use libc::{__error, getfsstat, statfs, MNT_NOWAIT};
use std::ffi::{CStr, OsStr};
use std::os::raw::{c_char, c_int};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use libc::{__error, getfsstat, MNT_NOWAIT, statfs};
use crate::fs::mounts::{Error, MountedFs};
use std::{mem, ptr};
/// Get a list of all mounted filesystem
pub fn mounts() -> Result<Vec<MountedFs>, Error> {
@ -36,7 +36,7 @@ pub fn mounts() -> Result<Vec<MountedFs>, Error> {
for mnt in &mntbuf {
let mount_point = OsStr::from_bytes(
// SAFETY: Converting null terminated "C" string
unsafe { CStr::from_ptr(mnt.f_mntonname.as_ptr().cast::<c_char>()) }.to_bytes()
unsafe { CStr::from_ptr(mnt.f_mntonname.as_ptr().cast::<c_char>()) }.to_bytes(),
);
let dest = PathBuf::from(mount_point);
// SAFETY: Converting null terminated "C" string
@ -47,8 +47,12 @@ pub fn mounts() -> Result<Vec<MountedFs>, Error> {
let source = unsafe { CStr::from_ptr(mnt.f_mntfromname.as_ptr().cast::<c_char>()) }
.to_string_lossy()
.into();
mounts.push(MountedFs { dest, fstype, source });
mounts.push(MountedFs {
dest,
fstype,
source,
});
}
Ok(mounts)
}
}

View File

@ -26,7 +26,7 @@ pub enum Error {
#[cfg(target_os = "macos")]
GetFSStatError(i32),
#[cfg(target_os = "linux")]
IOError(std::io::Error)
IOError(std::io::Error),
}
impl std::error::Error for Error {}
@ -39,8 +39,8 @@ impl std::fmt::Display for Error {
#[cfg(target_os = "macos")]
Error::GetFSStatError(err) => write!(f, "getfsstat failed: {err}"),
#[cfg(target_os = "linux")]
Error::IOError(err) => write!(f, "failed to read /proc/mounts: {err}"),
_ => write!(f, "Unknown error"),
Error::IOError(err) => write!(f, "failed to read /proc/mounts: {err}"),
_ => write!(f, "Unknown error"),
}
}
}
@ -64,7 +64,7 @@ pub(super) fn all_mounts() -> &'static HashMap<PathBuf, MountedFs> {
let mut mount_map: HashMap<PathBuf, MountedFs> = HashMap::new();
#[cfg(any(target_os = "linux", target_os = "macos"))]
if let Ok(mounts) = mounts() {
if let Ok(mounts) = mounts() {
for mount in mounts {
mount_map.insert(mount.dest.clone(), mount);
}
@ -72,4 +72,4 @@ pub(super) fn all_mounts() -> &'static HashMap<PathBuf, MountedFs> {
mount_map
})
}
}

View File

@ -22,9 +22,9 @@ pub enum FileType {
Compressed,
Temp,
Compiled,
Build // A “build file is something that can be run or activated somehow in order to
// kick off the build of a project. Its usually only present in directories full of
// source code.
Build, // A “build file is something that can be run or activated somehow in order to
// kick off the build of a project. Its usually only present in directories full of
// source code.
}
/// Mapping from full filenames to file type.
@ -270,20 +270,24 @@ impl FileType {
pub(crate) fn get_file_type(file: &File<'_>) -> Option<FileType> {
// Case-insensitive readme is checked first for backwards compatibility.
if file.name.to_lowercase().starts_with("readme") {
return Some(Self::Build)
return Some(Self::Build);
}
if let Some(file_type) = FILENAME_TYPES.get(&file.name) {
return Some(file_type.clone())
return Some(file_type.clone());
}
if let Some(file_type) = file.ext.as_ref().and_then(|ext| EXTENSION_TYPES.get(ext)) {
return Some(file_type.clone())
return Some(file_type.clone());
}
if file.name.ends_with('~') || (file.name.starts_with('#') && file.name.ends_with('#')) {
return Some(Self::Temp)
return Some(Self::Temp);
}
if let Some(dir) = file.parent_dir {
if file.get_source_files().iter().any(|path| dir.contains(path)) {
return Some(Self::Compiled)
if file
.get_source_files()
.iter()
.any(|path| dir.contains(path))
{
return Some(Self::Compiled);
}
}
None

View File

@ -2,9 +2,7 @@ use std::path::PathBuf;
use crate::fs::File;
impl<'a> File<'a> {
/// For this file, return a vector of alternate file paths that, if any of
/// them exist, mean that *this* file should be coloured as “compiled”.
///
@ -37,9 +35,8 @@ impl<'a> File<'a> {
_ => vec![], // No source files if none of the above
}
}
else {
vec![] // No source files if theres no extension, either!
} else {
vec![] // No source files if theres no extension, either!
}
}
}

View File

@ -2,8 +2,7 @@
use std::ffi::OsStr;
use ansiterm::{Colour, ANSIString};
use ansiterm::{ANSIString, Colour};
/// Sets the internal logger, changing the log level based on the value of an
/// environment variable.
@ -17,8 +16,7 @@ pub fn configure<T: AsRef<OsStr>>(ev: Option<T>) {
if env_var == "trace" {
log::set_max_level(log::LevelFilter::Trace);
}
else {
} else {
log::set_max_level(log::LevelFilter::Debug);
}
@ -28,7 +26,6 @@ pub fn configure<T: AsRef<OsStr>>(ev: Option<T>) {
}
}
#[derive(Debug)]
struct Logger;
@ -36,7 +33,7 @@ const GLOBAL_LOGGER: &Logger = &Logger;
impl log::Log for Logger {
fn enabled(&self, _: &log::Metadata<'_>) -> bool {
true // no need to filter after using set_max_level.
true // no need to filter after using set_max_level.
}
fn log(&self, record: &log::Record<'_>) {
@ -44,7 +41,14 @@ impl log::Log for Logger {
let level = level(record.level());
let close = Colour::Fixed(243).paint("]");
eprintln!("{}{} {}{} {}", open, level, record.target(), close, record.args());
eprintln!(
"{}{} {}{} {}",
open,
level,
record.target(),
close,
record.args()
);
}
fn flush(&self) {
@ -53,11 +57,12 @@ impl log::Log for Logger {
}
fn level(level: log::Level) -> ANSIString<'static> {
#[rustfmt::skip]
match level {
log::Level::Error => Colour::Red.paint("ERROR"),
log::Level::Warn => Colour::Yellow.paint("WARN"),
log::Level::Info => Colour::Cyan.paint("INFO"),
log::Level::Debug => Colour::Blue.paint("DEBUG"),
log::Level::Trace => Colour::Fixed(245).paint("TRACE"),
log::Level::Error => Colour::Red.paint("ERROR"),
log::Level::Warn => Colour::Yellow.paint("WARN"),
log::Level::Info => Colour::Cyan.paint("INFO"),
log::Level::Debug => Colour::Blue.paint("DEBUG"),
log::Level::Trace => Colour::Fixed(245).paint("TRACE"),
}
}

View File

@ -5,7 +5,6 @@
#![warn(rust_2018_idioms)]
#![warn(trivial_casts, trivial_numeric_casts)]
#![warn(unused)]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
@ -24,7 +23,7 @@
use std::env;
use std::ffi::{OsStr, OsString};
use std::io::{self, Write, ErrorKind};
use std::io::{self, ErrorKind, Write};
use std::path::{Component, PathBuf};
use std::process::exit;
@ -32,11 +31,11 @@ use ansiterm::{ANSIStrings, Style};
use log::*;
use crate::fs::{Dir, File};
use crate::fs::feature::git::GitCache;
use crate::fs::filter::GitIgnore;
use crate::options::{Options, Vars, vars, OptionsResult};
use crate::output::{escape, lines, grid, grid_details, details, View, Mode};
use crate::fs::{Dir, File};
use crate::options::{vars, Options, OptionsResult, Vars};
use crate::output::{details, escape, grid, grid_details, lines, Mode, View};
use crate::theme::Theme;
mod fs;
@ -62,20 +61,27 @@ fn main() {
let args: Vec<_> = env::args_os().skip(1).collect();
match Options::parse(args.iter().map(std::convert::AsRef::as_ref), &LiveVars) {
OptionsResult::Ok(options, mut input_paths) => {
// List the current directory by default.
// (This has to be done here, otherwise git_options wont see it.)
if input_paths.is_empty() {
input_paths = vec![ OsStr::new(".") ];
input_paths = vec![OsStr::new(".")];
}
let git = git_options(&options, &input_paths);
let writer = io::stdout();
let console_width = options.view.width.actual_terminal_width();
let theme = options.theme.to_theme(terminal_size::terminal_size().is_some());
let exa = Exa { options, writer, input_paths, theme, console_width, git };
let theme = options
.theme
.to_theme(terminal_size::terminal_size().is_some());
let exa = Exa {
options,
writer,
input_paths,
theme,
console_width,
git,
};
info!("matching on exa.run");
match exa.run() {
@ -117,10 +123,8 @@ fn main() {
}
}
/// The main program wrapper.
pub struct Exa<'args> {
/// List of command-line options, having been successfully parsed.
pub options: Options,
@ -161,8 +165,7 @@ impl Vars for LiveVars {
fn git_options(options: &Options, args: &[&OsStr]) -> Option<GitCache> {
if options.should_scan_for_git() {
Some(args.iter().map(PathBuf::from).collect())
}
else {
} else {
None
}
}
@ -179,25 +182,29 @@ impl<'args> Exa<'args> {
let mut exit_status = 0;
for file_path in &self.input_paths {
match File::from_args(PathBuf::from(file_path), None, None, self.options.view.deref_links) {
match File::from_args(
PathBuf::from(file_path),
None,
None,
self.options.view.deref_links,
) {
Err(e) => {
exit_status = 2;
writeln!(io::stderr(), "{file_path:?}: {e}")?;
}
Ok(f) => {
if f.points_to_directory() && ! self.options.dir_action.treat_dirs_as_files() {
if f.points_to_directory() && !self.options.dir_action.treat_dirs_as_files() {
trace!("matching on to_dir");
match f.to_dir() {
Ok(d) => dirs.push(d),
Ok(d) => dirs.push(d),
Err(e) if e.kind() == ErrorKind::PermissionDenied => {
warn!("Permission Denied: {e}");
exit(exits::PERMISSION_DENIED);
},
Err(e) => writeln!(io::stderr(), "{file_path:?}: {e}")?,
}
Err(e) => writeln!(io::stderr(), "{file_path:?}: {e}")?,
}
}
else {
} else {
files.push(f);
}
}
@ -217,52 +224,75 @@ impl<'args> Exa<'args> {
self.print_dirs(dirs, no_files, is_only_dir, exit_status)
}
fn print_dirs(&mut self, dir_files: Vec<Dir>, mut first: bool, is_only_dir: bool, exit_status: i32) -> io::Result<i32> {
fn print_dirs(
&mut self,
dir_files: Vec<Dir>,
mut first: bool,
is_only_dir: bool,
exit_status: i32,
) -> io::Result<i32> {
for dir in dir_files {
// Put a gap between directories, or between the list of files and
// the first directory.
if first {
first = false;
}
else {
} else {
writeln!(&mut self.writer)?;
}
if ! is_only_dir {
if !is_only_dir {
let mut bits = Vec::new();
escape(dir.path.display().to_string(), &mut bits, Style::default(), Style::default());
escape(
dir.path.display().to_string(),
&mut bits,
Style::default(),
Style::default(),
);
writeln!(&mut self.writer, "{}:", ANSIStrings(&bits))?;
}
let mut children = Vec::new();
let git_ignore = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
for file in dir.files(self.options.filter.dot_filter, self.git.as_ref(), git_ignore, self.options.view.deref_links) {
for file in dir.files(
self.options.filter.dot_filter,
self.git.as_ref(),
git_ignore,
self.options.view.deref_links,
) {
match file {
Ok(file) => children.push(file),
Err((path, e)) => writeln!(io::stderr(), "[{}: {}]", path.display(), e)?,
Ok(file) => children.push(file),
Err((path, e)) => writeln!(io::stderr(), "[{}: {}]", path.display(), e)?,
}
};
}
self.options.filter.filter_child_files(&mut children);
self.options.filter.sort_files(&mut children);
if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1;
if ! recurse_opts.tree && ! recurse_opts.is_too_deep(depth) {
let depth = dir
.path
.components()
.filter(|&c| c != Component::CurDir)
.count()
+ 1;
if !recurse_opts.tree && !recurse_opts.is_too_deep(depth) {
let mut child_dirs = Vec::new();
for child_dir in children.iter().filter(|f| f.is_directory() && ! f.is_all_all) {
for child_dir in children
.iter()
.filter(|f| f.is_directory() && !f.is_all_all)
{
match child_dir.to_dir() {
Ok(d) => child_dirs.push(d),
Err(e) => writeln!(io::stderr(), "{}: {}", child_dir.path.display(), e)?,
Ok(d) => child_dirs.push(d),
Err(e) => {
writeln!(io::stderr(), "{}: {}", child_dir.path.display(), e)?
}
}
}
self.print_files(Some(&dir), children)?;
match self.print_dirs(child_dirs, false, false, exit_status) {
Ok(_) => (),
Err(e) => return Err(e),
Ok(_) => (),
Err(e) => return Err(e),
}
continue;
}
@ -281,19 +311,34 @@ impl<'args> Exa<'args> {
}
let theme = &self.theme;
let View { ref mode, ref file_style, .. } = self.options.view;
let View {
ref mode,
ref file_style,
..
} = self.options.view;
match (mode, self.console_width) {
(Mode::Grid(ref opts), Some(console_width)) => {
let filter = &self.options.filter;
let r = grid::Render { files, theme, file_style, opts, console_width, filter };
let r = grid::Render {
files,
theme,
file_style,
opts,
console_width,
filter,
};
r.render(&mut self.writer)
}
(Mode::Grid(_), None) |
(Mode::Lines, _) => {
(Mode::Grid(_), None) | (Mode::Lines, _) => {
let filter = &self.options.filter;
let r = lines::Render { files, theme, file_style, filter };
let r = lines::Render {
files,
theme,
file_style,
filter,
};
r.render(&mut self.writer)
}
@ -303,7 +348,17 @@ impl<'args> Exa<'args> {
let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
let git = self.git.as_ref();
let r = details::Render { dir, files, theme, file_style, opts, recurse, filter, git_ignoring, git };
let r = details::Render {
dir,
files,
theme,
file_style,
opts,
recurse,
filter,
git_ignoring,
git,
};
r.render(&mut self.writer)
}
@ -316,7 +371,19 @@ impl<'args> Exa<'args> {
let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
let git = self.git.as_ref();
let r = grid_details::Render { dir, files, theme, file_style, grid, details, filter, row_threshold, git_ignoring, git, console_width };
let r = grid_details::Render {
dir,
files,
theme,
file_style,
grid,
details,
filter,
row_threshold,
git_ignoring,
git,
console_width,
};
r.render(&mut self.writer)
}
@ -327,14 +394,23 @@ impl<'args> Exa<'args> {
let git_ignoring = self.options.filter.git_ignore == GitIgnore::CheckAndIgnore;
let git = self.git.as_ref();
let r = details::Render { dir, files, theme, file_style, opts, recurse, filter, git_ignoring, git };
let r = details::Render {
dir,
files,
theme,
file_style,
opts,
recurse,
filter,
git_ignoring,
git,
};
r.render(&mut self.writer)
}
}
}
}
mod exits {
/// Exit code for when exa runs OK.

View File

@ -1,13 +1,11 @@
//! Parsing the options for `DirAction`.
use crate::options::parser::MatchedFlags;
use crate::options::{flags, OptionsError, NumberSource};
use crate::options::{flags, NumberSource, OptionsError};
use crate::fs::dir_action::{DirAction, RecurseOptions};
impl DirAction {
/// Determine which action to perform when trying to list a directory.
/// There are three possible actions, and they overlap somewhat: the
/// `--tree` flag is another form of recursion, so those two are allowed
@ -15,8 +13,9 @@ impl DirAction {
pub fn deduce(matches: &MatchedFlags<'_>, can_tree: bool) -> Result<Self, OptionsError> {
let recurse = matches.has(&flags::RECURSE)?;
let as_file = matches.has(&flags::LIST_DIRS)?;
let tree = matches.has(&flags::TREE)?;
let tree = matches.has(&flags::TREE)?;
#[rustfmt::skip]
if matches.is_strict() {
// Early check for --level when it wouldnt do anything
if ! recurse && ! tree && matches.count(&flags::LEVEL) > 0 {
@ -24,8 +23,7 @@ impl DirAction {
}
else if recurse && as_file {
return Err(OptionsError::Conflict(&flags::RECURSE, &flags::LIST_DIRS));
}
else if tree && as_file {
} else if tree && as_file {
return Err(OptionsError::Conflict(&flags::TREE, &flags::LIST_DIRS));
}
}
@ -34,22 +32,17 @@ impl DirAction {
// Tree is only appropriate in details mode, so this has to
// examine the View, which should have already been deduced by now
Ok(Self::Recurse(RecurseOptions::deduce(matches, true)?))
}
else if recurse {
} else if recurse {
Ok(Self::Recurse(RecurseOptions::deduce(matches, false)?))
}
else if as_file {
} else if as_file {
Ok(Self::AsFile)
}
else {
} else {
Ok(Self::List)
}
}
}
impl RecurseOptions {
/// Determine which files should be recursed into, based on the `--level`
/// flags value, and whether the `--tree` flag was passed, which was
/// determined earlier. The maximum level should be a number, and this
@ -58,22 +51,24 @@ impl RecurseOptions {
if let Some(level) = matches.get(&flags::LEVEL)? {
let arg_str = level.to_string_lossy();
match arg_str.parse() {
Ok(l) => {
Ok(Self { tree, max_depth: Some(l) })
}
Ok(l) => Ok(Self {
tree,
max_depth: Some(l),
}),
Err(e) => {
let source = NumberSource::Arg(&flags::LEVEL);
Err(OptionsError::FailedParse(arg_str.to_string(), source, e))
}
}
}
else {
Ok(Self { tree, max_depth: None })
} else {
Ok(Self {
tree,
max_depth: None,
})
}
}
}
#[cfg(test)]
mod test {
use super::*;
@ -88,15 +83,21 @@ mod test {
use crate::options::test::parse_for_test;
use crate::options::test::Strictnesses::*;
static TEST_ARGS: &[&Arg] = &[&flags::RECURSE, &flags::LIST_DIRS, &flags::TREE, &flags::LEVEL ];
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, true)) {
static TEST_ARGS: &[&Arg] = &[
&flags::RECURSE,
&flags::LIST_DIRS,
&flags::TREE,
&flags::LEVEL,
];
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf, true)
}) {
assert_eq!(result, $result);
}
}
};
}
// Default behaviour
test!(empty: DirAction <- []; Both => Ok(DirAction::List));
@ -125,7 +126,6 @@ mod test {
test!(dirs_tree_2: DirAction <- ["--list-dirs", "--tree"]; Complain => Err(OptionsError::Conflict(&flags::TREE, &flags::LIST_DIRS)));
test!(just_level_2: DirAction <- ["--level=4"]; Complain => Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)));
// Overriding levels
test!(overriding_1: DirAction <- ["-RL=6", "-L=7"]; Last => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(7) })));
test!(overriding_2: DirAction <- ["-RL=6", "-L=7"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'L'), Flag::Short(b'L'))));

View File

@ -5,11 +5,9 @@ use std::num::ParseIntError;
use crate::options::flags;
use crate::options::parser::{Arg, Flag, ParseError};
/// Something wrong with the combination of options the user has picked.
#[derive(PartialEq, Eq, Debug)]
pub enum OptionsError {
/// There was an error (from `getopts`) parsing the arguments.
Parse(ParseError),
@ -46,7 +44,6 @@ pub enum OptionsError {
/// The source of a string that failed to be parsed as a number.
#[derive(PartialEq, Eq, Debug)]
pub enum NumberSource {
/// It came... from a command-line argument!
Arg(&'static Arg),
@ -73,12 +70,18 @@ impl fmt::Display for OptionsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use crate::options::parser::TakesValue;
#[rustfmt::skip]
match self {
Self::BadArgument(arg, attempt) => {
if let TakesValue::Necessary(Some(values)) = arg.takes_value {
write!(f, "Option {} has no {:?} setting ({})", arg, attempt, Choices(values))
}
else {
write!(
f,
"Option {} has no {:?} setting ({})",
arg,
attempt,
Choices(values)
)
} else {
write!(f, "Option {arg} has no {attempt:?} setting")
}
}
@ -98,7 +101,6 @@ impl fmt::Display for OptionsError {
}
impl OptionsError {
/// Try to second-guess what the user was trying to do, depending on what
/// went wrong.
pub fn suggestion(&self) -> Option<&'static str> {
@ -110,14 +112,11 @@ impl OptionsError {
Self::Parse(ParseError::NeedsValue { ref flag, .. }) if *flag == Flag::Short(b't') => {
Some("To sort newest files last, try \"--sort newest\", or just \"-snew\"")
}
_ => {
None
}
_ => None,
}
}
}
/// A list of legal choices for an argument-taking option.
#[derive(PartialEq, Eq, Debug)]
pub struct Choices(pub &'static [&'static str]);

View File

@ -1,9 +1,8 @@
use crate::options::{flags, OptionsError, NumberSource};
use crate::options::parser::MatchedFlags;
use crate::options::vars::{self, Vars};
use crate::options::{flags, NumberSource, OptionsError};
use crate::output::file_name::{Options, Classify, ShowIcons, EmbedHyperlinks};
use crate::output::file_name::{Classify, EmbedHyperlinks, Options, ShowIcons};
impl Options {
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
@ -11,7 +10,11 @@ impl Options {
let show_icons = ShowIcons::deduce(matches, vars)?;
let embed_hyperlinks = EmbedHyperlinks::deduce(matches)?;
Ok(Self { classify, show_icons, embed_hyperlinks })
Ok(Self {
classify,
show_icons,
embed_hyperlinks,
})
}
}
@ -19,8 +22,11 @@ impl Classify {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
let flagged = matches.has(&flags::CLASSIFY)?;
if flagged { Ok(Self::AddFileIndicators) }
else { Ok(Self::JustFilenames) }
if flagged {
Ok(Self::AddFileIndicators)
} else {
Ok(Self::JustFilenames)
}
}
}
@ -28,19 +34,21 @@ impl ShowIcons {
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
if matches.has(&flags::NO_ICONS)? || !matches.has(&flags::ICONS)? {
Ok(Self::Off)
}
else if let Some(columns) = vars.get_with_fallback(vars::EZA_ICON_SPACING, vars::EXA_ICON_SPACING).and_then(|s| s.into_string().ok()) {
} else if let Some(columns) = vars
.get_with_fallback(vars::EZA_ICON_SPACING, vars::EXA_ICON_SPACING)
.and_then(|s| s.into_string().ok())
{
match columns.parse() {
Ok(width) => {
Ok(Self::On(width))
}
Ok(width) => Ok(Self::On(width)),
Err(e) => {
let source = NumberSource::Env(vars.source(vars::EZA_ICON_SPACING, vars::EXA_ICON_SPACING).unwrap());
let source = NumberSource::Env(
vars.source(vars::EZA_ICON_SPACING, vars::EXA_ICON_SPACING)
.unwrap(),
);
Err(OptionsError::FailedParse(columns, source, e))
}
}
}
else {
} else {
Ok(Self::On(1))
}
}
@ -50,7 +58,10 @@ impl EmbedHyperlinks {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
let flagged = matches.has(&flags::HYPERLINK)?;
if flagged { Ok(Self::On) }
else { Ok(Self::Off) }
if flagged {
Ok(Self::On)
} else {
Ok(Self::Off)
}
}
}

View File

@ -1,29 +1,28 @@
//! Parsing the options for `FileFilter`.
use crate::fs::filter::{FileFilter, FileFilterFlags, GitIgnore, IgnorePatterns, SortCase, SortField};
use crate::fs::DotFilter;
use crate::fs::filter::{FileFilter,FileFilterFlags, SortField, SortCase, IgnorePatterns, GitIgnore};
use crate::options::{flags, OptionsError};
use crate::options::parser::MatchedFlags;
use crate::options::{flags, OptionsError};
impl FileFilter {
/// Determines which of all the file filter options to use.
pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
use FileFilterFlags as FFF;
let mut filter_flags:Vec<FileFilterFlags> = vec![];
for (has,flag) in &[
(matches.has(&flags::REVERSE)?, FFF::Reverse),
(matches.has(&flags::ONLY_DIRS)?, FFF::OnlyDirs),
(matches.has(&flags::ONLY_FILES)?, FFF::OnlyFiles),
] {
if *has {
if *has {
filter_flags.push(flag.clone());
}
}
#[rustfmt::skip]
Ok(Self {
list_dirs_first: matches.has(&flags::DIRS_FIRST)?,
flags: filter_flags,
@ -36,7 +35,6 @@ impl FileFilter {
}
impl SortField {
/// Determines which sort field to use based on the `--sort` argument.
/// This arguments value can be one of several flags, listed above.
/// Returns the default sort field if none is given, or `Err` if the
@ -48,62 +46,32 @@ impl SortField {
let Some(word) = word.to_str() else { return Err(OptionsError::BadArgument(&flags::SORT, word.into())) };
let field = match word {
"name" | "filename" => {
Self::Name(SortCase::AaBbCc)
}
"Name" | "Filename" => {
Self::Name(SortCase::ABCabc)
}
".name" | ".filename" => {
Self::NameMixHidden(SortCase::AaBbCc)
}
".Name" | ".Filename" => {
Self::NameMixHidden(SortCase::ABCabc)
}
"size" | "filesize" => {
Self::Size
}
"ext" | "extension" => {
Self::Extension(SortCase::AaBbCc)
}
"Ext" | "Extension" => {
Self::Extension(SortCase::ABCabc)
}
"name" | "filename" => Self::Name(SortCase::AaBbCc),
"Name" | "Filename" => Self::Name(SortCase::ABCabc),
".name" | ".filename" => Self::NameMixHidden(SortCase::AaBbCc),
".Name" | ".Filename" => Self::NameMixHidden(SortCase::ABCabc),
"size" | "filesize" => Self::Size,
"ext" | "extension" => Self::Extension(SortCase::AaBbCc),
"Ext" | "Extension" => Self::Extension(SortCase::ABCabc),
// “new” sorts oldest at the top and newest at the bottom; “old”
// sorts newest at the top and oldest at the bottom. I think this
// is the right way round to do this: “size” puts the smallest at
// the top and the largest at the bottom, doesnt it?
"date" | "time" | "mod" | "modified" | "new" | "newest" => {
Self::ModifiedDate
}
"date" | "time" | "mod" | "modified" | "new" | "newest" => Self::ModifiedDate,
// Similarly, “age” means that files with the least age (the
// newest files) get sorted at the top, and files with the most
// age (the oldest) at the bottom.
"age" | "old" | "oldest" => {
Self::ModifiedAge
}
"age" | "old" | "oldest" => Self::ModifiedAge,
"ch" | "changed" => {
Self::ChangedDate
}
"acc" | "accessed" => {
Self::AccessedDate
}
"cr" | "created" => {
Self::CreatedDate
}
"ch" | "changed" => Self::ChangedDate,
"acc" | "accessed" => Self::AccessedDate,
"cr" | "created" => Self::CreatedDate,
#[cfg(unix)]
"inode" => {
Self::FileInode
}
"type" => {
Self::FileType
}
"none" => {
Self::Unsorted
}
"inode" => Self::FileInode,
"type" => Self::FileType,
"none" => Self::Unsorted,
_ => {
return Err(OptionsError::BadArgument(&flags::SORT, word.into()));
}
@ -113,7 +81,6 @@ impl SortField {
}
}
// Ive gone back and forth between whether to sort case-sensitively or
// insensitively by default. The default string sort in most programming
// languages takes each characters ASCII value into account, sorting
@ -151,9 +118,7 @@ impl Default for SortField {
}
}
impl DotFilter {
/// Determines the dot filter based on how many `--all` options were
/// given: one will show dotfiles, but two will show `.` and `..` too.
/// --almost-all is equivalent to --all, included for compatibility with
@ -175,25 +140,24 @@ impl DotFilter {
// either a single --all or at least one --almost-all is given
(1, _) | (0, true) => Ok(Self::Dotfiles),
// more than one --all
(c, _) => if matches.count(&flags::TREE) > 0 {
Err(OptionsError::TreeAllAll)
} else if matches.is_strict() && c > 2 {
Err(OptionsError::Conflict(&flags::ALL, &flags::ALL))
} else {
Ok(Self::DotfilesAndDots)
},
(c, _) => {
if matches.count(&flags::TREE) > 0 {
Err(OptionsError::TreeAllAll)
} else if matches.is_strict() && c > 2 {
Err(OptionsError::Conflict(&flags::ALL, &flags::ALL))
} else {
Ok(Self::DotfilesAndDots)
}
}
}
}
}
impl IgnorePatterns {
/// Determines the set of glob patterns to use based on the
/// `--ignore-glob` arguments value. This is a list of strings
/// separated by pipe (`|`) characters, given in any order.
pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
// If there are no inputs, we return a set of patterns that doesnt
// match anything, rather than, say, `None`.
let Some(inputs) = matches.get(&flags::IGNORE_GLOB)? else { return Ok(Self::empty()) };
@ -205,31 +169,28 @@ impl IgnorePatterns {
// It can actually return more than one glob error,
// but we only use one. (TODO)
match errors.pop() {
Some(e) => Err(e.into()),
None => Ok(patterns),
Some(e) => Err(e.into()),
None => Ok(patterns),
}
}
}
impl GitIgnore {
pub fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
if matches.has(&flags::GIT_IGNORE)? {
Ok(Self::CheckAndIgnore)
}
else {
} else {
Ok(Self::Off)
}
}
}
#[cfg(test)]
mod test {
use super::*;
use std::ffi::OsString;
use crate::options::flags;
use crate::options::parser::Flag;
use std::ffi::OsString;
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
@ -239,8 +200,17 @@ mod test {
use crate::options::test::parse_for_test;
use crate::options::test::Strictnesses::*;
static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::ALMOST_ALL, &flags::TREE, &flags::IGNORE_GLOB, &flags::GIT_IGNORE ];
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
static TEST_ARGS: &[&Arg] = &[
&flags::SORT,
&flags::ALL,
&flags::ALMOST_ALL,
&flags::TREE,
&flags::IGNORE_GLOB,
&flags::GIT_IGNORE,
];
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf)
}) {
assert_eq!(result, $result);
}
}
@ -278,7 +248,6 @@ mod test {
test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort"))));
}
mod dot_filters {
use super::*;
@ -304,7 +273,6 @@ mod test {
test!(almost_all_all_2: DotFilter <- ["-Aaa"]; Both => Ok(DotFilter::DotfilesAndDots));
}
mod ignore_patterns {
use super::*;
use std::iter::FromIterator;
@ -326,7 +294,6 @@ mod test {
test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I'))));
}
mod git_ignores {
use super::*;

View File

@ -1,6 +1,6 @@
#![rustfmt::skip] // the entire file becomes less readable with rustfmt
use crate::options::parser::{Arg, Args, TakesValue, Values};
// exa options
pub static VERSION: Arg = Arg { short: Some(b'v'), long: "version", takes_value: TakesValue::Forbidden };
pub static HELP: Arg = Arg { short: Some(b'?'), long: "help", takes_value: TakesValue::Forbidden };

View File

@ -4,7 +4,6 @@ use crate::fs::feature::xattr;
use crate::options::flags;
use crate::options::parser::MatchedFlags;
static USAGE_PART1: &str = "Usage:
eza [options] [files...]
@ -72,9 +71,9 @@ static GIT_VIEW_HELP: &str = " \
--git list each file's Git status, if tracked or ignored
--no-git suppress Git status (always overrides --git, --git-repos, --git-repos-no-status)
--git-repos list root of git-tree status";
static EXTENDED_HELP: &str = " \
static EXTENDED_HELP: &str = " \
-@, --extended list each file's extended attributes and sizes";
static SECATTR_HELP: &str = " \
static SECATTR_HELP: &str = " \
-Z, --context list each file's security context";
/// All the information needed to display the help text, which depends
@ -84,7 +83,6 @@ static SECATTR_HELP: &str = " \
pub struct HelpString;
impl HelpString {
/// Determines how to show help, if at all, based on the users
/// command-line arguments. This one works backwards from the other
/// deduce functions, returning Err if help needs to be shown.
@ -95,15 +93,13 @@ impl HelpString {
pub fn deduce(matches: &MatchedFlags<'_>) -> Option<Self> {
if matches.count(&flags::HELP) > 0 {
Some(Self)
}
else {
} else {
None
}
}
}
impl fmt::Display for HelpString {
/// Format this help options into an actual string of help
/// text to be displayed to the user.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
@ -128,7 +124,6 @@ impl fmt::Display for HelpString {
}
}
#[cfg(test)]
mod test {
use crate::options::{Options, OptionsResult};
@ -136,14 +131,14 @@ mod test {
#[test]
fn help() {
let args = vec![ OsStr::new("--help") ];
let args = vec![OsStr::new("--help")];
let opts = Options::parse(args, &None);
assert!(matches!(opts, OptionsResult::Help(_)));
}
#[test]
fn help_with_file() {
let args = vec![ OsStr::new("--help"), OsStr::new("me") ];
let args = vec![OsStr::new("--help"), OsStr::new("me")];
let opts = Options::parse(args, &None);
assert!(matches!(opts, OptionsResult::Help(_)));
}
@ -152,6 +147,6 @@ mod test {
fn unhelpful() {
let args = vec![];
let opts = Options::parse(args, &None);
assert!(! matches!(opts, OptionsResult::Help(_))) // no help when --help isnt passed
assert!(!matches!(opts, OptionsResult::Help(_))) // no help when --help isnt passed
}
}

View File

@ -68,12 +68,11 @@
//! --grid --long` shouldnt complain about `--long` being given twice when
//! its clear what the user wants.
use std::ffi::OsStr;
use crate::fs::dir_action::DirAction;
use crate::fs::filter::{FileFilter, GitIgnore};
use crate::output::{View, Mode, details, grid_details};
use crate::output::{details, grid_details, Mode, View};
use crate::theme::Options as ThemeOptions;
mod dir_action;
@ -84,7 +83,7 @@ mod theme;
mod view;
mod error;
pub use self::error::{OptionsError, NumberSource};
pub use self::error::{NumberSource, OptionsError};
mod help;
use self::help::HelpString;
@ -98,12 +97,10 @@ pub use self::vars::Vars;
mod version;
use self::version::VersionString;
/// These **options** represent a parsed, error-checked versions of the
/// users command-line options.
#[derive(Debug)]
pub struct Options {
/// The action to perform when encountering a directory rather than a
/// regular file.
pub dir_action: DirAction,
@ -122,17 +119,18 @@ pub struct Options {
}
impl Options {
/// Parse the given iterator of command-line strings into an Options
/// struct and a list of free filenames, using the environment variables
/// for extra options.
#[allow(unused_results)]
pub fn parse<'args, I, V>(args: I, vars: &V) -> OptionsResult<'args>
where I: IntoIterator<Item = &'args OsStr>,
V: Vars,
where
I: IntoIterator<Item = &'args OsStr>,
V: Vars,
{
use crate::options::parser::{Matches, Strictness};
#[rustfmt::skip]
let strictness = match vars.get_with_fallback(vars::EZA_STRICT, vars::EXA_STRICT) {
None => Strictness::UseLastArguments,
Some(ref t) if t.is_empty() => Strictness::UseLastArguments,
@ -140,8 +138,8 @@ impl Options {
};
let Matches { flags, frees } = match flags::ALL_ARGS.parse(args, strictness) {
Ok(m) => m,
Err(pe) => return OptionsResult::InvalidOptions(OptionsError::Parse(pe)),
Ok(m) => m,
Err(pe) => return OptionsResult::InvalidOptions(OptionsError::Parse(pe)),
};
if let Some(help) = HelpString::deduce(&flags) {
@ -153,8 +151,8 @@ impl Options {
}
match Self::deduce(&flags, vars) {
Ok(options) => OptionsResult::Ok(options, frees),
Err(oe) => OptionsResult::InvalidOptions(oe),
Ok(options) => OptionsResult::Ok(options, frees),
Err(oe) => OptionsResult::InvalidOptions(oe),
}
}
@ -167,8 +165,18 @@ impl Options {
}
match self.view.mode {
Mode::Details(details::Options { table: Some(ref table), .. }) |
Mode::GridDetails(grid_details::Options { details: details::Options { table: Some(ref table), .. }, .. }) => table.columns.git,
Mode::Details(details::Options {
table: Some(ref table),
..
})
| Mode::GridDetails(grid_details::Options {
details:
details::Options {
table: Some(ref table),
..
},
..
}) => table.columns.git,
_ => false,
}
}
@ -176,8 +184,11 @@ impl Options {
/// Determines the complete set of options based on the given command-line
/// arguments, after theyve been parsed.
fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
if cfg!(not(feature = "git")) &&
matches.has_where_any(|f| f.matches(&flags::GIT) || f.matches(&flags::GIT_IGNORE)).is_some() {
if cfg!(not(feature = "git"))
&& matches
.has_where_any(|f| f.matches(&flags::GIT) || f.matches(&flags::GIT_IGNORE))
.is_some()
{
return Err(OptionsError::Unsupported(String::from(
"Options --git and --git-ignore can't be used because `git` feature was disabled in this build of exa"
)));
@ -188,15 +199,18 @@ impl Options {
let filter = FileFilter::deduce(matches)?;
let theme = ThemeOptions::deduce(matches, vars)?;
Ok(Self { dir_action, filter, view, theme })
Ok(Self {
dir_action,
filter,
view,
theme,
})
}
}
/// The result of the `Options::getopts` function.
#[derive(Debug)]
pub enum OptionsResult<'args> {
/// The options were parsed successfully.
Ok(Options, Vec<&'args OsStr>),
@ -210,7 +224,6 @@ pub enum OptionsResult<'args> {
Version(VersionString),
}
#[cfg(test)]
pub mod test {
use crate::options::parser::{Arg, MatchedFlags};
@ -229,8 +242,14 @@ pub mod test {
///
/// It returns a vector with one or two elements in.
/// These elements can then be tested with `assert_eq` or what have you.
pub fn parse_for_test<T, F>(inputs: &[&str], args: &'static [&'static Arg], strictnesses: Strictnesses, get: F) -> Vec<T>
where F: Fn(&MatchedFlags<'_>) -> T
pub fn parse_for_test<T, F>(
inputs: &[&str],
args: &'static [&'static Arg],
strictnesses: Strictnesses,
get: F,
) -> Vec<T>
where
F: Fn(&MatchedFlags<'_>) -> T,
{
use self::Strictnesses::*;
use crate::options::parser::{Args, Strictness};

View File

@ -27,12 +27,10 @@
//! command-line options, as all the options and their values (such as
//! `--sort size`) are guaranteed to just be 8-bit ASCII.
use std::ffi::{OsStr, OsString};
use std::fmt;
use crate::options::error::{OptionsError, Choices};
use crate::options::error::{Choices, OptionsError};
/// A **short argument** is a single ASCII character.
pub type ShortArg = u8;
@ -61,8 +59,8 @@ pub enum Flag {
impl Flag {
pub fn matches(&self, arg: &Arg) -> bool {
match self {
Self::Short(short) => arg.short == Some(*short),
Self::Long(long) => arg.long == *long,
Self::Short(short) => arg.short == Some(*short),
Self::Long(long) => arg.long == *long,
}
}
}
@ -70,8 +68,8 @@ impl Flag {
impl fmt::Display for Flag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
Self::Short(short) => write!(f, "-{}", *short as char),
Self::Long(long) => write!(f, "--{long}"),
Self::Short(short) => write!(f, "-{}", *short as char),
Self::Long(long) => write!(f, "--{long}"),
}
}
}
@ -79,7 +77,6 @@ impl fmt::Display for Flag {
/// Whether redundant arguments should be considered a problem.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum Strictness {
/// Throw an error when an argument doesnt do anything, either because
/// it requires another argument to be specified, or because two conflict.
ComplainAboutRedundantArguments,
@ -93,7 +90,6 @@ pub enum Strictness {
/// arguments.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum TakesValue {
/// This flag has to be followed by a value.
/// If theres a fixed set of possible values, they can be printed out
/// with the error text.
@ -106,11 +102,9 @@ pub enum TakesValue {
Optional(Option<Values>),
}
/// An **argument** can be matched by one of the users input strings.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct Arg {
/// The short argument that matches it, if any.
pub short: Option<ShortArg>,
@ -134,17 +128,20 @@ impl fmt::Display for Arg {
}
}
/// Literally just several args.
#[derive(PartialEq, Eq, Debug)]
pub struct Args(pub &'static [&'static Arg]);
impl Args {
/// Iterates over the given list of command-line arguments and parses
/// them into a list of matched flags and free strings.
pub fn parse<'args, I>(&self, inputs: I, strictness: Strictness) -> Result<Matches<'args>, ParseError>
where I: IntoIterator<Item = &'args OsStr>
pub fn parse<'args, I>(
&self,
inputs: I,
strictness: Strictness,
) -> Result<Matches<'args>, ParseError>
where
I: IntoIterator<Item = &'args OsStr>,
{
let mut parsing = true;
@ -163,13 +160,11 @@ impl Args {
// This allows a file named “--arg” to be specified by passing in
// the pair “-- --arg”, without it getting matched as a flag that
// doesnt exist.
if ! parsing {
if !parsing {
frees.push(arg);
}
else if arg == "--" {
} else if arg == "--" {
parsing = false;
}
// If the string starts with *two* dashes then its a long argument.
else if bytes.starts_with(b"--") {
let long_arg_name = bytes_to_os_str(&bytes[2..]);
@ -181,12 +176,12 @@ impl Args {
let arg = self.lookup_long(before)?;
let flag = Flag::Long(arg.long);
match arg.takes_value {
TakesValue::Necessary(_) |
TakesValue::Optional(_) => result_flags.push((flag, Some(after))),
TakesValue::Forbidden => return Err(ParseError::ForbiddenValue { flag }),
TakesValue::Necessary(_) | TakesValue::Optional(_) => {
result_flags.push((flag, Some(after)))
}
TakesValue::Forbidden => return Err(ParseError::ForbiddenValue { flag }),
}
}
// If theres no equals, then the entire string (apart from
// the dashes) is the argument name.
else {
@ -199,23 +194,20 @@ impl Args {
TakesValue::Necessary(values) => {
if let Some(next_arg) = inputs.next() {
result_flags.push((flag, Some(next_arg)));
}
else {
return Err(ParseError::NeedsValue { flag, values })
} else {
return Err(ParseError::NeedsValue { flag, values });
}
}
TakesValue::Optional(_) => {
if let Some(next_arg) = inputs.next() {
result_flags.push((flag, Some(next_arg)));
}
else {
} else {
result_flags.push((flag, None));
}
}
}
}
}
// If the string starts with *one* dash then its one or more
// short arguments.
else if bytes.starts_with(b"-") && arg != "-" {
@ -234,15 +226,15 @@ impl Args {
// its an error if any of the first set of arguments actually
// takes a value.
if let Some((before, after)) = split_on_equals(short_arg) {
let (arg_with_value, other_args) = os_str_to_bytes(before).split_last().unwrap();
let (arg_with_value, other_args) =
os_str_to_bytes(before).split_last().unwrap();
// Process the characters immediately following the dash...
for byte in other_args {
let arg = self.lookup_short(*byte)?;
let flag = Flag::Short(*byte);
match arg.takes_value {
TakesValue::Forbidden |
TakesValue::Optional(_) => {
TakesValue::Forbidden | TakesValue::Optional(_) => {
result_flags.push((flag, None));
}
TakesValue::Necessary(values) => {
@ -255,8 +247,7 @@ impl Args {
let arg = self.lookup_short(*arg_with_value)?;
let flag = Flag::Short(arg.short.unwrap());
match arg.takes_value {
TakesValue::Necessary(_) |
TakesValue::Optional(_) => {
TakesValue::Necessary(_) | TakesValue::Optional(_) => {
result_flags.push((flag, Some(after)));
}
TakesValue::Forbidden => {
@ -264,7 +255,6 @@ impl Args {
}
}
}
// If theres no equals, then every character is parsed as
// its own short argument. However, if any of the arguments
// takes a value, then the *rest* of the string is used as
@ -285,17 +275,14 @@ impl Args {
TakesValue::Forbidden => {
result_flags.push((flag, None));
}
TakesValue::Necessary(values) |
TakesValue::Optional(values) => {
TakesValue::Necessary(values) | TakesValue::Optional(values) => {
if index < bytes.len() - 1 {
let remnants = &bytes[index+1 ..];
let remnants = &bytes[index + 1..];
result_flags.push((flag, Some(bytes_to_os_str(remnants))));
break;
}
else if let Some(next_arg) = inputs.next() {
} else if let Some(next_arg) = inputs.next() {
result_flags.push((flag, Some(next_arg)));
}
else {
} else {
match arg.takes_value {
TakesValue::Forbidden => {
unreachable!()
@ -313,36 +300,41 @@ impl Args {
}
}
}
// Otherwise, its a free string, usually a file name.
else {
frees.push(arg);
}
}
Ok(Matches { frees, flags: MatchedFlags { flags: result_flags, strictness } })
Ok(Matches {
frees,
flags: MatchedFlags {
flags: result_flags,
strictness,
},
})
}
fn lookup_short(&self, short: ShortArg) -> Result<&Arg, ParseError> {
match self.0.iter().find(|arg| arg.short == Some(short)) {
Some(arg) => Ok(arg),
None => Err(ParseError::UnknownShortArgument { attempt: short })
Some(arg) => Ok(arg),
None => Err(ParseError::UnknownShortArgument { attempt: short }),
}
}
fn lookup_long(&self, long: &OsStr) -> Result<&Arg, ParseError> {
match self.0.iter().find(|arg| arg.long == long) {
Some(arg) => Ok(arg),
None => Err(ParseError::UnknownArgument { attempt: long.to_os_string() })
Some(arg) => Ok(arg),
None => Err(ParseError::UnknownArgument {
attempt: long.to_os_string(),
}),
}
}
}
/// The **matches** are the result of parsing the users command-line strings.
#[derive(PartialEq, Eq, Debug)]
pub struct Matches<'args> {
/// The flags that were parsed from the users input.
pub flags: MatchedFlags<'args>,
@ -353,7 +345,6 @@ pub struct Matches<'args> {
#[derive(PartialEq, Eq, Debug)]
pub struct MatchedFlags<'args> {
/// The individual flags from the users input, in the order they were
/// originally given.
///
@ -367,7 +358,6 @@ pub struct MatchedFlags<'args> {
}
impl<'a> MatchedFlags<'a> {
/// Whether the given argument was specified.
/// Returns `true` if it was, `false` if it wasnt, and an error in
/// strict mode if it was specified more than once.
@ -382,16 +372,22 @@ impl<'a> MatchedFlags<'a> {
///
/// Youll have to test the resulting flag to see which argument it was.
pub fn has_where<P>(&self, predicate: P) -> Result<Option<&Flag>, OptionsError>
where P: Fn(&Flag) -> bool {
where
P: Fn(&Flag) -> bool,
{
if self.is_strict() {
let all = self.flags.iter()
.filter(|tuple| tuple.1.is_none() && predicate(&tuple.0))
.collect::<Vec<_>>();
let all = self
.flags
.iter()
.filter(|tuple| tuple.1.is_none() && predicate(&tuple.0))
.collect::<Vec<_>>();
if all.len() < 2 { Ok(all.first().map(|t| &t.0)) }
else { Err(OptionsError::Duplicate(all[0].0, all[1].0)) }
}
else {
if all.len() < 2 {
Ok(all.first().map(|t| &t.0))
} else {
Err(OptionsError::Duplicate(all[0].0, all[1].0))
}
} else {
Ok(self.has_where_any(predicate))
}
}
@ -401,8 +397,12 @@ impl<'a> MatchedFlags<'a> {
///
/// Youll have to test the resulting flag to see which argument it was.
pub fn has_where_any<P>(&self, predicate: P) -> Option<&Flag>
where P: Fn(&Flag) -> bool {
self.flags.iter().rev()
where
P: Fn(&Flag) -> bool,
{
self.flags
.iter()
.rev()
.find(|tuple| tuple.1.is_none() && predicate(&tuple.0))
.map(|tuple| &tuple.0)
}
@ -424,19 +424,28 @@ impl<'a> MatchedFlags<'a> {
///
/// Its not possible to tell which flag the value belonged to from this.
pub fn get_where<P>(&self, predicate: P) -> Result<Option<&OsStr>, OptionsError>
where P: Fn(&Flag) -> bool {
where
P: Fn(&Flag) -> bool,
{
if self.is_strict() {
let those = self.flags.iter()
.filter(|tuple| tuple.1.is_some() && predicate(&tuple.0))
.collect::<Vec<_>>();
let those = self
.flags
.iter()
.filter(|tuple| tuple.1.is_some() && predicate(&tuple.0))
.collect::<Vec<_>>();
if those.len() < 2 { Ok(those.first().copied().map(|t| t.1.unwrap())) }
else { Err(OptionsError::Duplicate(those[0].0, those[1].0)) }
}
else {
let found = self.flags.iter().rev()
.find(|tuple| tuple.1.is_some() && predicate(&tuple.0))
.map(|tuple| tuple.1.unwrap());
if those.len() < 2 {
Ok(those.first().copied().map(|t| t.1.unwrap()))
} else {
Err(OptionsError::Duplicate(those[0].0, those[1].0))
}
} else {
let found = self
.flags
.iter()
.rev()
.find(|tuple| tuple.1.is_some() && predicate(&tuple.0))
.map(|tuple| tuple.1.unwrap());
Ok(found)
}
}
@ -447,7 +456,8 @@ impl<'a> MatchedFlags<'a> {
/// Counts the number of occurrences of the given argument, even in
/// strict mode.
pub fn count(&self, arg: &Arg) -> usize {
self.flags.iter()
self.flags
.iter()
.filter(|tuple| tuple.0.matches(arg))
.count()
}
@ -459,12 +469,10 @@ impl<'a> MatchedFlags<'a> {
}
}
/// A problem with the users input that meant it couldnt be parsed into a
/// coherent list of arguments.
#[derive(PartialEq, Eq, Debug)]
pub enum ParseError {
/// A flag that has to take a value was not given one.
NeedsValue { flag: Flag, values: Option<Values> },
@ -484,36 +492,43 @@ pub enum ParseError {
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NeedsValue { flag, values: None } => write!(f, "Flag {flag} needs a value"),
Self::NeedsValue { flag, values: Some(cs) } => write!(f, "Flag {flag} needs a value ({})", Choices(cs)),
Self::ForbiddenValue { flag } => write!(f, "Flag {flag} cannot take a value"),
Self::UnknownShortArgument { attempt } => write!(f, "Unknown argument -{}", *attempt as char),
Self::UnknownArgument { attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()),
Self::NeedsValue { flag, values: None } => write!(f, "Flag {flag} needs a value"),
Self::NeedsValue {
flag,
values: Some(cs),
} => write!(f, "Flag {flag} needs a value ({})", Choices(cs)),
Self::ForbiddenValue { flag } => write!(f, "Flag {flag} cannot take a value"),
Self::UnknownShortArgument { attempt } => {
write!(f, "Unknown argument -{}", *attempt as char)
}
Self::UnknownArgument { attempt } => {
write!(f, "Unknown argument --{}", attempt.to_string_lossy())
}
}
}
}
#[cfg(unix)]
fn os_str_to_bytes(s: &OsStr) -> &[u8]{
fn os_str_to_bytes(s: &OsStr) -> &[u8] {
use std::os::unix::ffi::OsStrExt;
return s.as_bytes()
return s.as_bytes();
}
#[cfg(unix)]
fn bytes_to_os_str(b: &[u8]) -> &OsStr{
fn bytes_to_os_str(b: &[u8]) -> &OsStr {
use std::os::unix::ffi::OsStrExt;
return OsStr::from_bytes(b);
}
#[cfg(windows)]
fn os_str_to_bytes(s: &OsStr) -> &[u8]{
return s.to_str().unwrap().as_bytes()
fn os_str_to_bytes(s: &OsStr) -> &[u8] {
return s.to_str().unwrap().as_bytes();
}
#[cfg(windows)]
fn bytes_to_os_str(b: &[u8]) -> &OsStr{
fn bytes_to_os_str(b: &[u8]) -> &OsStr {
use std::str;
return OsStr::new(str::from_utf8(b).unwrap());
@ -526,16 +541,14 @@ fn split_on_equals(input: &OsStr) -> Option<(&OsStr, &OsStr)> {
let (before, after) = os_str_to_bytes(input).split_at(index);
// The after string contains the = that we need to remove.
if ! before.is_empty() && after.len() >= 2 {
return Some((bytes_to_os_str(before),
bytes_to_os_str(&after[1..])))
if !before.is_empty() && after.len() >= 2 {
return Some((bytes_to_os_str(before), bytes_to_os_str(&after[1..])));
}
}
None
}
#[cfg(test)]
mod split_test {
use super::split_on_equals;
@ -545,16 +558,17 @@ mod split_test {
($name:ident: $input:expr => None) => {
#[test]
fn $name() {
assert_eq!(split_on_equals(&OsString::from($input)),
None);
assert_eq!(split_on_equals(&OsString::from($input)), None);
}
};
($name:ident: $input:expr => $before:expr, $after:expr) => {
#[test]
fn $name() {
assert_eq!(split_on_equals(&OsString::from($input)),
Some((OsStr::new($before), OsStr::new($after))));
assert_eq!(
split_on_equals(&OsString::from($input)),
Some((OsStr::new($before), OsStr::new($after)))
);
}
};
}
@ -571,7 +585,6 @@ mod split_test {
test_split!(more: "this=that=other" => "this", "that=other");
}
#[cfg(test)]
mod parse_test {
use super::*;
@ -580,16 +593,15 @@ mod parse_test {
($name:ident: $inputs:expr => frees: $frees:expr, flags: $flags:expr) => {
#[test]
fn $name() {
let inputs: &[&'static str] = $inputs.as_ref();
let inputs = inputs.iter().map(OsStr::new);
let frees: &[&'static str] = $frees.as_ref();
let frees = frees.iter().map(OsStr::new).collect();
let frees = frees.iter().map(OsStr::new).collect();
let flags = <[_]>::into_vec(Box::new($flags));
let strictness = Strictness::UseLastArguments; // this isnt even used
let strictness = Strictness::UseLastArguments; // this isnt even used
let got = Args(TEST_ARGS).parse(inputs, strictness);
let flags = MatchedFlags { flags, strictness };
@ -605,15 +617,16 @@ mod parse_test {
let inputs = $inputs.iter().map(OsStr::new);
let strictness = Strictness::UseLastArguments; // this isnt even used
let strictness = Strictness::UseLastArguments; // this isnt even used
let got = Args(TEST_ARGS).parse(inputs, strictness);
assert_eq!(got, Err($error));
}
};
}
const SUGGESTIONS: Values = &[ "example" ];
const SUGGESTIONS: Values = &["example"];
#[rustfmt::skip]
static TEST_ARGS: &[&Arg] = &[
&Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden },
&Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden },
@ -621,7 +634,6 @@ mod parse_test {
&Arg { short: Some(b't'), long: "type", takes_value: TakesValue::Necessary(Some(SUGGESTIONS)) }
];
// Just filenames
test!(empty: [] => frees: [], flags: []);
test!(one_arg: ["exa"] => frees: [ "exa" ], flags: []);
@ -633,7 +645,6 @@ mod parse_test {
test!(two_arg_l: ["--", "--long"] => frees: [ "--long" ], flags: []);
test!(two_arg_s: ["--", "-l"] => frees: [ "-l" ], flags: []);
// Long args
test!(long: ["--long"] => frees: [], flags: [ (Flag::Long("long"), None) ]);
test!(long_then: ["--long", "4"] => frees: [ "4" ], flags: [ (Flag::Long("long"), None) ]);
@ -650,7 +661,6 @@ mod parse_test {
test!(arg_equals_s: ["--type=exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsStr::new("exa"))) ]);
test!(arg_then_s: ["--type", "exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsStr::new("exa"))) ]);
// Short args
test!(short: ["-l"] => frees: [], flags: [ (Flag::Short(b'l'), None) ]);
test!(short_then: ["-l", "4"] => frees: [ "4" ], flags: [ (Flag::Short(b'l'), None) ]);
@ -672,7 +682,6 @@ mod parse_test {
test!(short_two_equals_s: ["-t=exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]);
test!(short_two_next_s: ["-t", "exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]);
// Unknown args
test!(unknown_long: ["--quiet"] => error UnknownArgument { attempt: OsString::from("quiet") });
test!(unknown_long_eq: ["--quiet=shhh"] => error UnknownArgument { attempt: OsString::from("quiet") });
@ -682,7 +691,6 @@ mod parse_test {
test!(unknown_short_2nd_eq: ["-lq=shhh"] => error UnknownShortArgument { attempt: b'q' });
}
#[cfg(test)]
mod matches_test {
use super::*;
@ -701,9 +709,16 @@ mod matches_test {
};
}
static VERBOSE: Arg = Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden };
static COUNT: Arg = Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary(None) };
static VERBOSE: Arg = Arg {
short: Some(b'v'),
long: "verbose",
takes_value: TakesValue::Forbidden,
};
static COUNT: Arg = Arg {
short: Some(b'c'),
long: "count",
takes_value: TakesValue::Necessary(None),
};
test!(short_never: [], has VERBOSE => false);
test!(short_once: [(Flag::Short(b'v'), None)], has VERBOSE => true);
@ -712,13 +727,12 @@ mod matches_test {
test!(long_twice: [(Flag::Long("verbose"), None), (Flag::Long("verbose"), None)], has VERBOSE => true);
test!(long_mixed: [(Flag::Long("verbose"), None), (Flag::Short(b'v'), None)], has VERBOSE => true);
#[test]
fn only_count() {
let everything = OsString::from("everything");
let flags = MatchedFlags {
flags: vec![ (Flag::Short(b'c'), Some(&*everything)) ],
flags: vec![(Flag::Short(b'c'), Some(&*everything))],
strictness: Strictness::UseLastArguments,
};
@ -728,11 +742,13 @@ mod matches_test {
#[test]
fn rightmost_count() {
let everything = OsString::from("everything");
let nothing = OsString::from("nothing");
let nothing = OsString::from("nothing");
let flags = MatchedFlags {
flags: vec![ (Flag::Short(b'c'), Some(&*everything)),
(Flag::Short(b'c'), Some(&*nothing)) ],
flags: vec![
(Flag::Short(b'c'), Some(&*everything)),
(Flag::Short(b'c'), Some(&*nothing)),
],
strictness: Strictness::UseLastArguments,
};
@ -741,7 +757,10 @@ mod matches_test {
#[test]
fn no_count() {
let flags = MatchedFlags { flags: Vec::new(), strictness: Strictness::UseLastArguments };
let flags = MatchedFlags {
flags: Vec::new(),
strictness: Strictness::UseLastArguments,
};
assert!(!flags.has(&COUNT).unwrap());
}

View File

@ -1,7 +1,6 @@
use crate::options::{flags, vars, Vars, OptionsError};
use crate::options::parser::MatchedFlags;
use crate::theme::{Options, UseColours, ColourScale, Definitions};
use crate::options::{flags, vars, OptionsError, Vars};
use crate::theme::{ColourScale, Definitions, Options, UseColours};
impl Options {
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
@ -9,17 +8,19 @@ impl Options {
let colour_scale = ColourScale::deduce(matches)?;
let definitions = if use_colours == UseColours::Never {
Definitions::default()
}
else {
Definitions::deduce(vars)
};
Definitions::default()
} else {
Definitions::deduce(vars)
};
Ok(Self { use_colours, colour_scale, definitions })
Ok(Self {
use_colours,
colour_scale,
definitions,
})
}
}
impl UseColours {
fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
let default_value = match vars.get(vars::NO_COLOR) {
@ -31,61 +32,65 @@ impl UseColours {
if word == "always" {
Ok(Self::Always)
}
else if word == "auto" || word == "automatic" {
} else if word == "auto" || word == "automatic" {
Ok(Self::Automatic)
}
else if word == "never" {
} else if word == "never" {
Ok(Self::Never)
}
else {
} else {
Err(OptionsError::BadArgument(&flags::COLOR, word.into()))
}
}
}
impl ColourScale {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
if matches.has_where(|f| f.matches(&flags::COLOR_SCALE) || f.matches(&flags::COLOUR_SCALE))?.is_some() {
if matches
.has_where(|f| f.matches(&flags::COLOR_SCALE) || f.matches(&flags::COLOUR_SCALE))?
.is_some()
{
Ok(Self::Gradient)
}
else {
} else {
Ok(Self::Fixed)
}
}
}
impl Definitions {
fn deduce<V: Vars>(vars: &V) -> Self {
let ls = vars.get(vars::LS_COLORS)
let ls = vars
.get(vars::LS_COLORS)
.map(|e| e.to_string_lossy().to_string());
let exa = vars.get_with_fallback(vars::EZA_COLORS, vars::EXA_COLORS)
let exa = vars
.get_with_fallback(vars::EZA_COLORS, vars::EXA_COLORS)
.map(|e| e.to_string_lossy().to_string());
Self { ls, exa }
}
}
#[cfg(test)]
mod terminal_test {
use super::*;
use std::ffi::OsString;
use crate::options::flags;
use crate::options::parser::{Flag, Arg};
use crate::options::parser::{Arg, Flag};
use std::ffi::OsString;
use crate::options::test::parse_for_test;
use crate::options::test::Strictnesses::*;
static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR,
&flags::COLOR_SCALE, &flags::COLOUR_SCALE, ];
static TEST_ARGS: &[&Arg] = &[
&flags::COLOR,
&flags::COLOUR,
&flags::COLOR_SCALE,
&flags::COLOUR_SCALE,
];
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf)
}) {
assert_eq!(result, $result);
}
}
@ -95,7 +100,9 @@ mod terminal_test {
#[test]
fn $name() {
let env = $env;
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &env)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf, &env)
}) {
assert_eq!(result, $result);
}
}
@ -104,7 +111,9 @@ mod terminal_test {
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => err $result:expr) => {
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf)
}) {
assert_eq!(result.unwrap_err(), $result);
}
}
@ -114,7 +123,9 @@ mod terminal_test {
#[test]
fn $name() {
let env = $env;
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &env)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf, &env)
}) {
assert_eq!(result.unwrap_err(), $result);
}
}
@ -147,23 +158,19 @@ mod terminal_test {
// Test impl that just returns the value it has.
impl Vars for MockVars {
fn get(&self, name: &'static str) -> Option<OsString> {
if name == vars::LS_COLORS && ! self.ls.is_empty() {
if name == vars::LS_COLORS && !self.ls.is_empty() {
Some(OsString::from(self.ls))
}
else if (name == vars::EZA_COLORS || name == vars::EXA_COLORS) && ! self.exa.is_empty() {
} else if (name == vars::EZA_COLORS || name == vars::EXA_COLORS) && !self.exa.is_empty()
{
Some(OsString::from(self.exa))
}
else if name == vars::NO_COLOR && ! self.no_color.is_empty() {
} else if name == vars::NO_COLOR && !self.no_color.is_empty() {
Some(OsString::from(self.no_color))
}
else {
} else {
None
}
}
}
// Default
test!(empty: UseColours <- [], MockVars::empty(); Both => Ok(UseColours::Automatic));
test!(empty_with_no_color: UseColours <- [], MockVars::with_no_color(); Both => Ok(UseColours::Never));

View File

@ -1,6 +1,5 @@
use std::ffi::OsString;
// General variables
/// Environment variable used to colour files, both by their filesystem type
@ -53,7 +52,6 @@ pub static EZA_GRID_ROWS: &str = "EZA_GRID_ROWS";
pub static EXA_ICON_SPACING: &str = "EXA_ICON_SPACING";
pub static EZA_ICON_SPACING: &str = "EZA_ICON_SPACING";
/// Mockable wrapper for `std::env::var_os`.
pub trait Vars {
fn get(&self, name: &'static str) -> Option<OsString>;
@ -69,12 +67,11 @@ pub trait Vars {
fn source(&self, name: &'static str, fallback: &'static str) -> Option<&'static str> {
match self.get(name) {
Some(_) => Some(name),
None => self.get(fallback).and(Some(fallback)),
None => self.get(fallback).and(Some(fallback)),
}
}
}
// Test impl that just returns the value it has.
#[cfg(test)]
impl Vars for Option<OsString> {

View File

@ -7,13 +7,11 @@ use std::fmt;
use crate::options::flags;
use crate::options::parser::MatchedFlags;
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct VersionString;
// There were options here once, but there arent anymore!
impl VersionString {
/// Determines how to show the version, if at all, based on the users
/// command-line arguments. This one works backwards from the other
/// deduce functions, returning Err if help needs to be shown.
@ -22,8 +20,7 @@ impl VersionString {
pub fn deduce(matches: &MatchedFlags<'_>) -> Option<Self> {
if matches.count(&flags::VERSION) > 0 {
Some(Self)
}
else {
} else {
None
}
}
@ -31,11 +28,14 @@ impl VersionString {
impl fmt::Display for VersionString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{}", include_str!(concat!(env!("OUT_DIR"), "/version_string.txt")))
write!(
f,
"{}",
include_str!(concat!(env!("OUT_DIR"), "/version_string.txt"))
)
}
}
#[cfg(test)]
mod test {
use crate::options::{Options, OptionsResult};
@ -43,14 +43,14 @@ mod test {
#[test]
fn version() {
let args = vec![ OsStr::new("--version") ];
let args = vec![OsStr::new("--version")];
let opts = Options::parse(args, &None);
assert!(matches!(opts, OptionsResult::Version(_)));
}
#[test]
fn version_with_file() {
let args = vec![ OsStr::new("--version"), OsStr::new("me") ];
let args = vec![OsStr::new("--version"), OsStr::new("me")];
let opts = Options::parse(args, &None);
assert!(matches!(opts, OptionsResult::Version(_)));
}

View File

@ -1,12 +1,11 @@
use crate::fs::feature::xattr;
use crate::options::{flags, OptionsError, NumberSource, Vars};
use crate::options::parser::MatchedFlags;
use crate::output::{View, Mode, TerminalWidth, grid, details};
use crate::output::grid_details::{self, RowThreshold};
use crate::options::{flags, NumberSource, OptionsError, Vars};
use crate::output::file_name::Options as FileStyle;
use crate::output::table::{TimeTypes, SizeFormat, UserFormat, Columns, Options as TableOptions};
use crate::output::grid_details::{self, RowThreshold};
use crate::output::table::{Columns, Options as TableOptions, SizeFormat, TimeTypes, UserFormat};
use crate::output::time::TimeFormat;
use crate::output::{details, grid, Mode, TerminalWidth, View};
impl View {
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
@ -14,13 +13,16 @@ impl View {
let width = TerminalWidth::deduce(matches, vars)?;
let file_style = FileStyle::deduce(matches, vars)?;
let deref_links = matches.has(&flags::DEREF_LINKS)?;
Ok(Self { mode, width, file_style, deref_links })
Ok(Self {
mode,
width,
file_style,
deref_links,
})
}
}
impl Mode {
/// Determine which viewing mode to use based on the users options.
///
/// As with the other options, arguments are scanned right-to-left and the
@ -30,8 +32,12 @@ impl Mode {
/// This is complicated a little by the fact that `--grid` and `--tree`
/// can also combine with `--long`, so care has to be taken to use the
pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
let flag = matches.has_where_any(|f| f.matches(&flags::LONG) || f.matches(&flags::ONE_LINE)
|| f.matches(&flags::GRID) || f.matches(&flags::TREE));
let flag = matches.has_where_any(|f| {
f.matches(&flags::LONG)
|| f.matches(&flags::ONE_LINE)
|| f.matches(&flags::GRID)
|| f.matches(&flags::TREE)
});
let Some(flag) = flag else {
Self::strict_check_long_flags(matches)?;
@ -40,19 +46,24 @@ impl Mode {
};
if flag.matches(&flags::LONG)
|| (flag.matches(&flags::TREE) && matches.has(&flags::LONG)?)
|| (flag.matches(&flags::GRID) && matches.has(&flags::LONG)?)
|| (flag.matches(&flags::TREE) && matches.has(&flags::LONG)?)
|| (flag.matches(&flags::GRID) && matches.has(&flags::LONG)?)
{
let _ = matches.has(&flags::LONG)?;
let details = details::Options::deduce_long(matches, vars)?;
let flag = matches.has_where_any(|f| f.matches(&flags::GRID) || f.matches(&flags::TREE));
let flag =
matches.has_where_any(|f| f.matches(&flags::GRID) || f.matches(&flags::TREE));
if flag.is_some() && flag.unwrap().matches(&flags::GRID) {
let _ = matches.has(&flags::GRID)?;
let grid = grid::Options::deduce(matches)?;
let row_threshold = RowThreshold::deduce(vars)?;
let grid_details = grid_details::Options { grid, details, row_threshold };
let grid_details = grid_details::Options {
grid,
details,
row_threshold,
};
return Ok(Self::GridDetails(grid_details));
}
@ -81,9 +92,18 @@ impl Mode {
// If --long hasnt been passed, then check if we need to warn the
// user about flags that wont have any effect.
if matches.is_strict() {
for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS,
&flags::HEADER, &flags::BLOCKSIZE, &flags::TIME, &flags::GROUP, &flags::NUMERIC,
&flags::MOUNTS ] {
for option in &[
&flags::BINARY,
&flags::BYTES,
&flags::INODE,
&flags::LINKS,
&flags::HEADER,
&flags::BLOCKSIZE,
&flags::TIME,
&flags::GROUP,
&flags::NUMERIC,
&flags::MOUNTS,
] {
if matches.has(option)? {
return Err(OptionsError::Useless(option, false, &flags::LONG));
}
@ -91,9 +111,15 @@ impl Mode {
if matches.has(&flags::GIT)? && !matches.has(&flags::NO_GIT)? {
return Err(OptionsError::Useless(&flags::GIT, false, &flags::LONG));
}
else if matches.has(&flags::LEVEL)? && ! matches.has(&flags::RECURSE)? && ! matches.has(&flags::TREE)? {
return Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE));
} else if matches.has(&flags::LEVEL)?
&& !matches.has(&flags::RECURSE)?
&& !matches.has(&flags::TREE)?
{
return Err(OptionsError::Useless2(
&flags::LEVEL,
&flags::RECURSE,
&flags::TREE,
));
}
}
@ -101,7 +127,6 @@ impl Mode {
}
}
impl grid::Options {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
let grid = grid::Options {
@ -112,7 +137,6 @@ impl grid::Options {
}
}
impl details::Options {
fn deduce_tree(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
let details = details::Options {
@ -128,10 +152,9 @@ impl details::Options {
fn deduce_long<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
if matches.is_strict() {
if matches.has(&flags::ACROSS)? && ! matches.has(&flags::GRID)? {
if matches.has(&flags::ACROSS)? && !matches.has(&flags::GRID)? {
return Err(OptionsError::Useless(&flags::ACROSS, true, &flags::LONG));
}
else if matches.has(&flags::ONE_LINE)? {
} else if matches.has(&flags::ONE_LINE)? {
return Err(OptionsError::Useless(&flags::ONE_LINE, true, &flags::LONG));
}
}
@ -146,7 +169,6 @@ impl details::Options {
}
}
impl TerminalWidth {
fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
use crate::options::vars;
@ -166,84 +188,99 @@ impl TerminalWidth {
Err(OptionsError::FailedParse(arg_str.to_string(), source, e))
}
}
}
else if let Some(columns) = vars.get(vars::COLUMNS).and_then(|s| s.into_string().ok()) {
} else if let Some(columns) = vars.get(vars::COLUMNS).and_then(|s| s.into_string().ok()) {
match columns.parse() {
Ok(width) => {
Ok(Self::Set(width))
}
Ok(width) => Ok(Self::Set(width)),
Err(e) => {
let source = NumberSource::Env(vars::COLUMNS);
Err(OptionsError::FailedParse(columns, source, e))
}
}
}
else {
} else {
Ok(Self::Automatic)
}
}
}
impl RowThreshold {
fn deduce<V: Vars>(vars: &V) -> Result<Self, OptionsError> {
use crate::options::vars;
if let Some(columns) = vars.get_with_fallback(vars::EZA_GRID_ROWS, vars::EXA_GRID_ROWS).and_then(|s| s.into_string().ok()) {
if let Some(columns) = vars
.get_with_fallback(vars::EZA_GRID_ROWS, vars::EXA_GRID_ROWS)
.and_then(|s| s.into_string().ok())
{
match columns.parse() {
Ok(rows) => {
Ok(Self::MinimumRows(rows))
}
Ok(rows) => Ok(Self::MinimumRows(rows)),
Err(e) => {
let source = NumberSource::Env(vars.source(vars::EZA_GRID_ROWS, vars::EXA_GRID_ROWS).unwrap());
let source = NumberSource::Env(
vars.source(vars::EZA_GRID_ROWS, vars::EXA_GRID_ROWS)
.unwrap(),
);
Err(OptionsError::FailedParse(columns, source, e))
}
}
}
else {
} else {
Ok(Self::AlwaysGrid)
}
}
}
impl TableOptions {
fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
let time_format = TimeFormat::deduce(matches, vars)?;
let size_format = SizeFormat::deduce(matches)?;
let user_format = UserFormat::deduce(matches)?;
let columns = Columns::deduce(matches)?;
Ok(Self { size_format, time_format, user_format, columns })
Ok(Self {
size_format,
time_format,
user_format,
columns,
})
}
}
impl Columns {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
let time_types = TimeTypes::deduce(matches)?;
let git = matches.has(&flags::GIT)? && !matches.has(&flags::NO_GIT)?;
let subdir_git_repos = matches.has(&flags::GIT_REPOS)? && !matches.has(&flags::NO_GIT)?;
let subdir_git_repos_no_stat = !subdir_git_repos && matches.has(&flags::GIT_REPOS_NO_STAT)? && !matches.has(&flags::NO_GIT)?;
let subdir_git_repos_no_stat = !subdir_git_repos
&& matches.has(&flags::GIT_REPOS_NO_STAT)?
&& !matches.has(&flags::NO_GIT)?;
let blocksize = matches.has(&flags::BLOCKSIZE)?;
let group = matches.has(&flags::GROUP)?;
let inode = matches.has(&flags::INODE)?;
let links = matches.has(&flags::LINKS)?;
let octal = matches.has(&flags::OCTAL)?;
let blocksize = matches.has(&flags::BLOCKSIZE)?;
let group = matches.has(&flags::GROUP)?;
let inode = matches.has(&flags::INODE)?;
let links = matches.has(&flags::LINKS)?;
let octal = matches.has(&flags::OCTAL)?;
let security_context = xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?;
let permissions = ! matches.has(&flags::NO_PERMISSIONS)?;
let filesize = ! matches.has(&flags::NO_FILESIZE)?;
let user = ! matches.has(&flags::NO_USER)?;
let permissions = !matches.has(&flags::NO_PERMISSIONS)?;
let filesize = !matches.has(&flags::NO_FILESIZE)?;
let user = !matches.has(&flags::NO_USER)?;
Ok(Self { time_types, inode, links, blocksize, group, git, subdir_git_repos, subdir_git_repos_no_stat, octal, security_context, permissions, filesize, user })
Ok(Self {
time_types,
inode,
links,
blocksize,
group,
git,
subdir_git_repos,
subdir_git_repos_no_stat,
octal,
security_context,
permissions,
filesize,
user,
})
}
}
impl SizeFormat {
/// Determine which file size to use in the file size column based on
/// the users options.
///
@ -256,42 +293,37 @@ impl SizeFormat {
let flag = matches.has_where(|f| f.matches(&flags::BINARY) || f.matches(&flags::BYTES))?;
Ok(match flag {
Some(f) if f.matches(&flags::BINARY) => Self::BinaryBytes,
Some(f) if f.matches(&flags::BYTES) => Self::JustBytes,
_ => Self::DecimalBytes,
Some(f) if f.matches(&flags::BINARY) => Self::BinaryBytes,
Some(f) if f.matches(&flags::BYTES) => Self::JustBytes,
_ => Self::DecimalBytes,
})
}
}
impl TimeFormat {
/// Determine how time should be formatted in timestamp columns.
fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
let word =
if let Some(w) = matches.get(&flags::TIME_STYLE)? {
w.to_os_string()
let word = if let Some(w) = matches.get(&flags::TIME_STYLE)? {
w.to_os_string()
} else {
use crate::options::vars;
match vars.get(vars::TIME_STYLE) {
Some(ref t) if !t.is_empty() => t.clone(),
_ => return Ok(Self::DefaultFormat),
}
else {
use crate::options::vars;
match vars.get(vars::TIME_STYLE) {
Some(ref t) if ! t.is_empty() => t.clone(),
_ => return Ok(Self::DefaultFormat)
}
};
};
match word.to_string_lossy().as_ref() {
"default" => Ok(Self::DefaultFormat),
"default" => Ok(Self::DefaultFormat),
"relative" => Ok(Self::Relative),
"iso" => Ok(Self::ISOFormat),
"iso" => Ok(Self::ISOFormat),
"long-iso" => Ok(Self::LongISO),
"full-iso" => Ok(Self::FullISO),
_ => Err(OptionsError::BadArgument(&flags::TIME_STYLE, word))
_ => Err(OptionsError::BadArgument(&flags::TIME_STYLE, word)),
}
}
}
impl UserFormat {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
let flag = matches.has(&flags::NUMERIC)?;
@ -299,9 +331,7 @@ impl UserFormat {
}
}
impl TimeTypes {
/// Determine which of a files time fields should be displayed for it
/// based on the users options.
///
@ -315,47 +345,48 @@ impl TimeTypes {
fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
let possible_word = matches.get(&flags::TIME)?;
let modified = matches.has(&flags::MODIFIED)?;
let changed = matches.has(&flags::CHANGED)?;
let changed = matches.has(&flags::CHANGED)?;
let accessed = matches.has(&flags::ACCESSED)?;
let created = matches.has(&flags::CREATED)?;
let created = matches.has(&flags::CREATED)?;
let no_time = matches.has(&flags::NO_TIME)?;
let time_types = if no_time {
Self { modified: false, changed: false, accessed: false, created: false }
Self {
modified: false,
changed: false,
accessed: false,
created: false,
}
} else if let Some(word) = possible_word {
#[rustfmt::skip]
if modified {
return Err(OptionsError::Useless(&flags::MODIFIED, true, &flags::TIME));
}
else if changed {
} else if changed {
return Err(OptionsError::Useless(&flags::CHANGED, true, &flags::TIME));
}
else if accessed {
} else if accessed {
return Err(OptionsError::Useless(&flags::ACCESSED, true, &flags::TIME));
}
else if created {
} else if created {
return Err(OptionsError::Useless(&flags::CREATED, true, &flags::TIME));
}
else if word == "mod" || word == "modified" {
} else if word == "mod" || word == "modified" {
Self { modified: true, changed: false, accessed: false, created: false }
}
else if word == "ch" || word == "changed" {
} else if word == "ch" || word == "changed" {
Self { modified: false, changed: true, accessed: false, created: false }
}
else if word == "acc" || word == "accessed" {
} else if word == "acc" || word == "accessed" {
Self { modified: false, changed: false, accessed: true, created: false }
}
else if word == "cr" || word == "created" {
} else if word == "cr" || word == "created" {
Self { modified: false, changed: false, accessed: false, created: true }
}
else {
} else {
return Err(OptionsError::BadArgument(&flags::TIME, word.into()));
}
}
else if modified || changed || accessed || created {
Self { modified, changed, accessed, created }
}
else {
} else if modified || changed || accessed || created {
Self {
modified,
changed,
accessed,
created,
}
} else {
Self::default()
};
@ -363,33 +394,49 @@ impl TimeTypes {
}
}
#[cfg(test)]
mod test {
use super::*;
use std::ffi::OsString;
use crate::options::flags;
use crate::options::parser::{Flag, Arg};
use crate::options::parser::{Arg, Flag};
use std::ffi::OsString;
use crate::options::test::parse_for_test;
use crate::options::test::Strictnesses::*;
static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES, &flags::TIME_STYLE,
&flags::TIME, &flags::MODIFIED, &flags::CHANGED,
&flags::CREATED, &flags::ACCESSED,
&flags::HEADER, &flags::GROUP, &flags::INODE, &flags::GIT,
&flags::LINKS, &flags::BLOCKSIZE, &flags::LONG, &flags::LEVEL,
&flags::GRID, &flags::ACROSS, &flags::ONE_LINE, &flags::TREE,
&flags::NUMERIC ];
static TEST_ARGS: &[&Arg] = &[
&flags::BINARY,
&flags::BYTES,
&flags::TIME_STYLE,
&flags::TIME,
&flags::MODIFIED,
&flags::CHANGED,
&flags::CREATED,
&flags::ACCESSED,
&flags::HEADER,
&flags::GROUP,
&flags::INODE,
&flags::GIT,
&flags::LINKS,
&flags::BLOCKSIZE,
&flags::LONG,
&flags::LEVEL,
&flags::GRID,
&flags::ACROSS,
&flags::ONE_LINE,
&flags::TREE,
&flags::NUMERIC,
];
macro_rules! test {
($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => {
/// Macro that writes a test.
/// If testing both strictnesses, theyll both be done in the same function.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf)
}) {
assert_eq!(result, $result);
}
}
@ -400,7 +447,9 @@ mod test {
/// This is needed because sometimes the Ok type doesnt implement `PartialEq`.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf)
}) {
assert_eq!(result.unwrap_err(), $result);
}
}
@ -411,22 +460,25 @@ mod test {
/// Instead of using `PartialEq`, this just tests if it matches a pat.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf)
}) {
println!("Testing {:?}", result);
match result {
$pat => assert!(true),
_ => assert!(false),
_ => assert!(false),
}
}
}
};
($name:ident: $type:ident <- $inputs:expr, $vars:expr; $stricts:expr => err $result:expr) => {
/// Like above, but with $vars.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &$vars)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf, &$vars)
}) {
assert_eq!(result.unwrap_err(), $result);
}
}
@ -436,18 +488,19 @@ mod test {
/// Like further above, but with $vars.
#[test]
fn $name() {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &$vars)) {
for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| {
$type::deduce(mf, &$vars)
}) {
println!("Testing {:?}", result);
match result {
$pat => assert!(true),
_ => assert!(false),
_ => assert!(false),
}
}
}
};
}
mod size_formats {
use super::*;
@ -470,7 +523,6 @@ mod test {
test!(both_8: SizeFormat <- ["--bytes", "--bytes"]; Complain => err OptionsError::Duplicate(Flag::Long("bytes"), Flag::Long("bytes")));
}
mod time_formats {
use super::*;
@ -505,7 +557,6 @@ mod test {
test!(override_env: TimeFormat <- ["--time-style=full-iso"], Some("long-iso".into()); Both => like Ok(TimeFormat::FullISO));
}
mod time_types {
use super::*;
@ -541,7 +592,6 @@ mod test {
// Multiples
test!(time_uu: TimeTypes <- ["-u", "--modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: true, created: false }));
// Errors
test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err OptionsError::BadArgument(&flags::TIME, OsString::from("tea")));
test!(t_ea: TimeTypes <- ["-tea"]; Both => err OptionsError::BadArgument(&flags::TIME, OsString::from("ea")));
@ -551,13 +601,11 @@ mod test {
test!(overridden_2: TimeTypes <- ["-tcr", "-tmod"]; Complain => err OptionsError::Duplicate(Flag::Short(b't'), Flag::Short(b't')));
}
mod views {
use super::*;
use crate::output::grid::Options as GridOptions;
// Default
test!(empty: Mode <- [], None; Both => like Ok(Mode::Grid(_)));

View File

@ -3,10 +3,9 @@
use std::iter::Sum;
use std::ops::{Add, Deref, DerefMut};
use ansiterm::{Style, ANSIString, ANSIStrings};
use ansiterm::{ANSIString, ANSIStrings, Style};
use unicode_width::UnicodeWidthStr;
/// 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.
///
@ -19,7 +18,6 @@ use unicode_width::UnicodeWidthStr;
/// type by that name too.)
#[derive(PartialEq, Debug, Clone, Default)]
pub struct TextCell {
/// The contents of this cell, as a vector of ANSI-styled strings.
pub contents: TextCellContents,
@ -36,14 +34,13 @@ impl Deref for TextCell {
}
impl TextCell {
/// Creates a new text cell that holds the given text in the given style,
/// computing the Unicode width of the text.
pub fn paint(style: Style, text: String) -> Self {
let width = DisplayWidth::from(&*text);
Self {
contents: vec![ style.paint(text) ].into(),
contents: vec![style.paint(text)].into(),
width,
}
}
@ -55,7 +52,7 @@ impl TextCell {
let width = DisplayWidth::from(text);
Self {
contents: vec![ style.paint(text) ].into(),
contents: vec![style.paint(text)].into(),
width,
}
}
@ -67,6 +64,7 @@ impl TextCell {
/// This is used in place of empty table cells, as it is easier to read
/// tabular data when there is *something* in each cell.
pub fn blank(style: Style) -> Self {
#[rustfmt::skip]
Self {
contents: vec![ style.paint("-") ].into(),
width: DisplayWidth::from(1),
@ -96,7 +94,6 @@ impl TextCell {
}
}
// Id like to eventually abstract cells so that instead of *every* cell
// storing a vector, only variable-length cells would, and individual cells
// would just store an array of a fixed length (which would usually be just 1
@ -123,7 +120,6 @@ impl TextCell {
//
// But exa still has bugs and I need to fix those first :(
/// The contents of a text cell, as a vector of ANSI-styled strings.
///
/// Its possible to use this type directly in the case where you want a
@ -152,7 +148,6 @@ impl Deref for TextCellContents {
// above can just access the value directly.
impl TextCellContents {
/// Produces an `ANSIStrings` value that can be used to print the styled
/// values of this cell as an ANSI-terminal-formatted string.
pub fn strings(&self) -> ANSIStrings<'_> {
@ -162,7 +157,8 @@ impl TextCellContents {
/// 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 {
self.0.iter()
self.0
.iter()
.map(|anstr| DisplayWidth::from(&**anstr))
.sum()
}
@ -177,7 +173,6 @@ impl TextCellContents {
}
}
/// The Unicode “display width” of a string.
///
/// This is related to the number of *graphemes* of a string, rather than the
@ -238,13 +233,13 @@ impl Add<usize> for DisplayWidth {
impl Sum for DisplayWidth {
fn sum<I>(iter: I) -> Self
where I: Iterator<Item = Self>
where
I: Iterator<Item = Self>,
{
iter.fold(Self(0), Add::add)
}
}
#[cfg(test)]
mod width_unit_test {
use super::DisplayWidth;

View File

@ -59,7 +59,6 @@
//! means that we must wait until every row has been added to the table before it
//! can be displayed, in order to make sure that every column is wide enough.
use std::io::{self, Write};
use std::mem::MaybeUninit;
use std::path::PathBuf;
@ -70,19 +69,18 @@ use scoped_threadpool::Pool;
use log::*;
use crate::fs::{Dir, File};
use crate::fs::dir_action::RecurseOptions;
use crate::fs::feature::git::GitCache;
use crate::fs::feature::xattr::Attribute;
use crate::fs::fields::SecurityContextType;
use crate::fs::filter::FileFilter;
use crate::fs::{Dir, File};
use crate::output::cell::TextCell;
use crate::output::file_name::Options as FileStyle;
use crate::output::table::{Table, Options as TableOptions, Row as TableRow};
use crate::output::tree::{TreeTrunk, TreeParams, TreeDepth};
use crate::output::table::{Options as TableOptions, Row as TableRow, Table};
use crate::output::tree::{TreeDepth, TreeParams, TreeTrunk};
use crate::theme::Theme;
/// With the **Details** view, the output gets formatted into columns, with
/// each `Column` object showing some piece of information about the file,
/// such as its size, or its permissions.
@ -94,10 +92,10 @@ use crate::theme::Theme;
///
/// Almost all the heavy lifting is done in a Table object, which handles the
/// columns for each row.
#[allow(clippy::struct_excessive_bools)] /// This clearly isn't a state machine
#[allow(clippy::struct_excessive_bools)]
/// This clearly isn't a state machine
#[derive(PartialEq, Eq, Debug)]
pub struct Options {
/// Options specific to drawing a table.
///
/// Directories themselves can pick which columns are *added* to this
@ -117,7 +115,6 @@ pub struct Options {
pub mounts: bool,
}
pub struct Render<'a> {
pub dir: Option<&'a Dir>,
pub files: Vec<File<'a>>,
@ -139,7 +136,7 @@ pub struct Render<'a> {
pub git: Option<&'a GitCache>,
}
#[rustfmt::skip]
struct Egg<'a> {
table_row: Option<TableRow>,
xattrs: &'a [Attribute],
@ -154,7 +151,6 @@ impl<'a> AsRef<File<'a>> for Egg<'a> {
}
}
impl<'a> Render<'a> {
pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
let n_cpus = match num_cpus::get() as u32 {
@ -165,6 +161,7 @@ impl<'a> Render<'a> {
let mut rows = Vec::new();
if let Some(ref table) = self.opts.table {
#[rustfmt::skip]
match (self.git, self.dir) {
(Some(g), Some(d)) => if ! g.has_anything_for(&d.path) { self.git = None },
(Some(g), None) => if ! self.files.iter().any(|f| g.has_anything_for(&f.path)) { self.git = None },
@ -182,14 +179,25 @@ impl<'a> Render<'a> {
// This is weird, but I cant find a way around it:
// https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6
let mut table = Some(table);
self.add_files_to_table(&mut pool, &mut table, &mut rows, &self.files, TreeDepth::root());
self.add_files_to_table(
&mut pool,
&mut table,
&mut rows,
&self.files,
TreeDepth::root(),
);
for row in self.iterate_with_table(table.unwrap(), rows) {
writeln!(w, "{}", row.strings())?;
}
}
else {
self.add_files_to_table(&mut pool, &mut None, &mut rows, &self.files, TreeDepth::root());
} else {
self.add_files_to_table(
&mut pool,
&mut None,
&mut rows,
&self.files,
TreeDepth::root(),
);
for row in self.iterate(rows) {
writeln!(w, "{}", row.strings())?;
@ -204,20 +212,30 @@ impl<'a> Render<'a> {
// Do not show the hint '@' if the only extended attribute is the security
// attribute and the security attribute column is active.
let xattr_count = file.extended_attributes().len();
let selinux_ctx_shown = self.opts.secattr && match file.security_context().context {
SecurityContextType::SELinux(_) => true,
SecurityContextType::None => false,
};
let selinux_ctx_shown = self.opts.secattr
&& match file.security_context().context {
SecurityContextType::SELinux(_) => true,
SecurityContextType::None => false,
};
xattr_count > 1 || (xattr_count == 1 && !selinux_ctx_shown)
}
/// Adds files to the table, possibly recursively. This is easily
/// parallelisable, and uses a pool of threads.
fn add_files_to_table<'dir>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], depth: TreeDepth) {
use std::sync::{Arc, Mutex};
fn add_files_to_table<'dir>(
&self,
pool: &mut Pool,
table: &mut Option<Table<'a>>,
rows: &mut Vec<Row>,
src: &[File<'dir>],
depth: TreeDepth,
) {
use crate::fs::feature::xattr;
use std::sync::{Arc, Mutex};
let mut file_eggs = (0..src.len()).map(|_| MaybeUninit::uninit()).collect::<Vec<_>>();
let mut file_eggs = (0..src.len())
.map(|_| MaybeUninit::uninit())
.collect::<Vec<_>>();
pool.scoped(|scoped| {
let file_eggs = Arc::new(Mutex::new(&mut file_eggs));
@ -257,12 +275,13 @@ impl<'a> Render<'a> {
&[]
};
let table_row = table.as_ref()
.map(|t| t.row_for_file(file, self.show_xattr_hint(file)));
let table_row = table
.as_ref()
.map(|t| t.row_for_file(file, self.show_xattr_hint(file)));
let mut dir = None;
if let Some(r) = self.recurse {
if file.is_directory() && r.tree && ! r.is_too_deep(depth.0) {
if file.is_directory() && r.tree && !r.is_too_deep(depth.0) {
trace!("matching on to_dir");
match file.to_dir() {
Ok(d) => {
@ -275,7 +294,13 @@ impl<'a> Render<'a> {
}
};
let egg = Egg { table_row, xattrs, errors, dir, file };
let egg = Egg {
table_row,
xattrs,
errors,
dir,
file,
};
unsafe { std::ptr::write(file_eggs.lock().unwrap()[idx].as_mut_ptr(), egg) }
});
}
@ -293,22 +318,29 @@ impl<'a> Render<'a> {
t.add_widths(row);
}
let file_name = self.file_style.for_file(egg.file, self.theme)
.with_link_paths()
.with_mount_details(self.opts.mounts)
.paint()
.promote();
let file_name = self
.file_style
.for_file(egg.file, self.theme)
.with_link_paths()
.with_mount_details(self.opts.mounts)
.paint()
.promote();
let row = Row {
tree: tree_params,
cells: egg.table_row,
name: file_name,
tree: tree_params,
cells: egg.table_row,
name: file_name,
};
rows.push(row);
if let Some(ref dir) = egg.dir {
for file_to_add in dir.files(self.filter.dot_filter, self.git, self.git_ignoring, egg.file.deref_links) {
for file_to_add in dir.files(
self.filter.dot_filter,
self.git,
self.git_ignoring,
egg.file.deref_links,
) {
match file_to_add {
Ok(f) => {
files.push(f);
@ -321,13 +353,17 @@ impl<'a> Render<'a> {
self.filter.filter_child_files(&mut files);
if ! files.is_empty() {
if !files.is_empty() {
for xattr in egg.xattrs {
rows.push(self.render_xattr(xattr, TreeParams::new(depth.deeper(), false)));
}
for (error, path) in errors {
rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), false), path));
rows.push(self.render_error(
&error,
TreeParams::new(depth.deeper(), false),
path,
));
}
self.add_files_to_table(pool, table, rows, &files, depth.deeper());
@ -337,7 +373,8 @@ impl<'a> Render<'a> {
let count = egg.xattrs.len();
for (index, xattr) in egg.xattrs.iter().enumerate() {
let params = TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1);
let params =
TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1);
let r = self.render_xattr(xattr, params);
rows.push(r);
}
@ -353,9 +390,9 @@ impl<'a> Render<'a> {
pub fn render_header(&self, header: TableRow) -> Row {
Row {
tree: TreeParams::new(TreeDepth::root(), false),
cells: Some(header),
name: TextCell::paint_str(self.theme.ui.header, "Name"),
tree: TreeParams::new(TreeDepth::root(), false),
cells: Some(header),
name: TextCell::paint_str(self.theme.ui.header, "Name"),
}
}
@ -371,16 +408,31 @@ impl<'a> Render<'a> {
// TODO: broken_symlink() doesnt quite seem like the right name for
// the style thats being used here. Maybe split it in two?
let name = TextCell::paint(self.theme.broken_symlink(), error_message);
Row { cells: None, name, tree }
Row {
cells: None,
name,
tree,
}
}
fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{}=\"{}\"", xattr.name, xattr.value));
Row { cells: None, name, tree }
let name = TextCell::paint(
self.theme.ui.perms.attribute,
format!("{}=\"{}\"", xattr.name, xattr.value),
);
Row {
cells: None,
name,
tree,
}
}
pub fn render_file(&self, cells: TableRow, name: TextCell, tree: TreeParams) -> Row {
Row { cells: Some(cells), name, tree }
Row {
cells: Some(cells),
name,
tree,
}
}
pub fn iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a> {
@ -402,9 +454,7 @@ impl<'a> Render<'a> {
}
}
pub struct Row {
/// Vector of cells to display.
///
/// Most of the rows will be used to display files metadata, so this will
@ -421,7 +471,7 @@ pub struct Row {
pub tree: TreeParams,
}
#[rustfmt::skip]
pub struct TableIter<'a> {
inner: VecIntoIter<Row>,
table: Table<'a>,
@ -436,15 +486,13 @@ impl<'a> Iterator for TableIter<'a> {
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|row| {
let mut cell =
if let Some(cells) = row.cells {
self.table.render(cells)
}
else {
let mut cell = TextCell::default();
cell.add_spaces(self.total_width);
cell
};
let mut cell = if let Some(cells) = row.cells {
self.table.render(cells)
} else {
let mut cell = TextCell::default();
cell.add_spaces(self.total_width);
cell
};
for tree_part in self.tree_trunk.new_row(row.tree) {
cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
@ -452,7 +500,7 @@ impl<'a> Iterator for TableIter<'a> {
// If any tree characters have been printed, then add an extra
// space, which makes the output look much better.
if ! row.tree.is_at_root() {
if !row.tree.is_at_root() {
cell.add_spaces(1);
}
@ -462,7 +510,6 @@ impl<'a> Iterator for TableIter<'a> {
}
}
pub struct Iter {
tree_trunk: TreeTrunk,
tree_style: Style,
@ -482,7 +529,7 @@ impl Iterator for Iter {
// If any tree characters have been printed, then add an extra
// space, which makes the output look much better.
if ! row.tree.is_at_root() {
if !row.tree.is_at_root() {
cell.add_spaces(1);
}

View File

@ -1,6 +1,5 @@
use ansiterm::{ANSIString, Style};
pub fn escape(string: String, bits: &mut Vec<ANSIString<'_>>, good: Style, bad: Style) {
// if the string has no control character
if string.chars().all(|c| !c.is_control()) {

View File

@ -13,29 +13,34 @@ use crate::output::render::FiletypeColours;
/// Basically a file name factory.
#[derive(Debug, Copy, Clone)]
pub struct Options {
/// Whether to append file class characters to file names.
pub classify: Classify,
/// Whether to prepend icon characters before file names.
pub show_icons: ShowIcons,
/// Whether to make file names hyperlinks.
pub embed_hyperlinks: EmbedHyperlinks,
}
impl Options {
/// Create a new `FileName` that prints the given files name, painting it
/// with the remaining arguments.
pub fn for_file<'a, 'dir, C>(self, file: &'a File<'dir>, colours: &'a C) -> FileName<'a, 'dir, C> {
pub fn for_file<'a, 'dir, C>(
self,
file: &'a File<'dir>,
colours: &'a C,
) -> FileName<'a, 'dir, C> {
FileName {
file,
colours,
link_style: LinkStyle::JustFilenames,
options: self,
target: if file.is_link() { Some(file.link_target()) }
else { None },
options: self,
target: if file.is_link() {
Some(file.link_target())
} else {
None
},
mount_style: MountStyle::JustDirectoryNames,
mounted_fs: file.mount_point_info(),
}
@ -46,7 +51,6 @@ impl Options {
/// links, depending on how long the resulting Cell can be.
#[derive(PartialEq, Debug, Copy, Clone)]
enum LinkStyle {
/// Just display the file names, but colour them differently if theyre
/// a broken link or cant be followed.
JustFilenames,
@ -57,11 +61,9 @@ enum LinkStyle {
FullLinkPaths,
}
/// Whether to append file class characters to the file names.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum Classify {
/// Just display the file names, without any characters.
JustFilenames,
@ -80,7 +82,6 @@ impl Default for Classify {
/// mount details, depending on how long the resulting Cell can be.
#[derive(PartialEq, Debug, Copy, Clone)]
enum MountStyle {
/// Just display the directory names.
JustDirectoryNames,
@ -92,7 +93,6 @@ enum MountStyle {
/// Whether and how to show icons.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum ShowIcons {
/// Dont show icons at all.
Off,
@ -103,8 +103,7 @@ pub enum ShowIcons {
/// Whether to embed hyperlinks.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum EmbedHyperlinks{
pub enum EmbedHyperlinks {
Off,
On,
}
@ -112,7 +111,6 @@ pub enum EmbedHyperlinks{
/// 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, C> {
/// A reference to the file that were getting the name of.
file: &'a File<'dir>,
@ -120,7 +118,7 @@ pub struct FileName<'a, 'dir, C> {
colours: &'a C,
/// The file that this file points to if its a link.
target: Option<FileTarget<'dir>>, // todo: remove?
target: Option<FileTarget<'dir>>, // todo: remove?
/// How to handle displaying links.
link_style: LinkStyle,
@ -135,7 +133,6 @@ pub struct FileName<'a, 'dir, C> {
}
impl<'a, 'dir, C> FileName<'a, 'dir, C> {
/// Sets the flag on this file name to display link targets with an
/// arrow followed by their path.
pub fn with_link_paths(mut self) -> Self {
@ -156,7 +153,6 @@ impl<'a, 'dir, C> FileName<'a, 'dir, C> {
}
impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
/// Paints the name of the file using the colours, resulting in a vector
/// of coloured cells that can be printed to the terminal.
///
@ -185,13 +181,13 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
}
}
if ! self.file.name.is_empty() {
// The “missing file” colour seems like it should be used here,
// but its not! In a grid view, where theres no space to display
// link targets, the filename has to have a different style to
// indicate this fact. But when showing targets, we can just
// colour the path instead (see below), and leave the broken
// links filename as the link colour.
if !self.file.name.is_empty() {
// The “missing file” colour seems like it should be used here,
// but its not! In a grid view, where theres no space to display
// link targets, the filename has to have a different style to
// indicate this fact. But when showing targets, we can just
// colour the path instead (see below), and leave the broken
// links filename as the link colour.
for bit in self.escaped_file_name() {
bits.push(bit);
}
@ -208,7 +204,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
self.add_parent_bits(&mut bits, parent);
}
if ! target.name.is_empty() {
if !target.name.is_empty() {
let target_options = Options {
classify: Classify::JustFilenames,
show_icons: ShowIcons::Off,
@ -254,14 +250,15 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
// Do nothing — the error gets displayed on the next line
}
}
}
else if let Classify::AddFileIndicators = self.options.classify {
} else if let Classify::AddFileIndicators = self.options.classify {
if let Some(class) = self.classify_char(self.file) {
bits.push(Style::default().paint(class));
}
}
if let (MountStyle::MountInfo, Some(mount_details)) = (self.mount_style, self.mounted_fs.as_ref()) {
if let (MountStyle::MountInfo, Some(mount_details)) =
(self.mount_style, self.mounted_fs.as_ref())
{
// This is a filesystem mounted on the directory, output its details
bits.push(Style::default().paint(" ["));
bits.push(Style::default().paint(mount_details.source.clone()));
@ -279,16 +276,23 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
let coconut = parent.components().count();
if coconut == 1 && parent.has_root() {
bits.push(self.colours.symlink_path().paint(std::path::MAIN_SEPARATOR.to_string()));
}
else if coconut >= 1 {
bits.push(
self.colours
.symlink_path()
.paint(std::path::MAIN_SEPARATOR.to_string()),
);
} else if coconut >= 1 {
escape(
parent.to_string_lossy().to_string(),
bits,
self.colours.symlink_path(),
self.colours.control_char(),
);
bits.push(self.colours.symlink_path().paint(std::path::MAIN_SEPARATOR.to_string()));
bits.push(
self.colours
.symlink_path()
.paint(std::path::MAIN_SEPARATOR.to_string()),
);
}
}
@ -298,20 +302,15 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
fn classify_char(&self, file: &File<'_>) -> Option<&'static str> {
if file.is_executable_file() {
Some("*")
}
else if file.is_directory() {
} else if file.is_directory() {
Some("/")
}
else if file.is_pipe() {
} else if file.is_pipe() {
Some("|")
}
else if file.is_link() {
} else if file.is_link() {
Some("@")
}
else if file.is_socket() {
} else if file.is_socket() {
Some("=")
}
else {
} else {
None
}
}
@ -320,11 +319,9 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
fn classify_char(&self, file: &File<'_>) -> Option<&'static str> {
if file.is_directory() {
Some("/")
}
else if file.is_link() {
} else if file.is_link() {
Some("@")
}
else {
} else {
None
}
}
@ -342,7 +339,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
/// So in that situation, those characters will be escaped and highlighted in
/// a different colour.
fn escaped_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
use percent_encoding::{CONTROLS, utf8_percent_encode};
use percent_encoding::{utf8_percent_encode, CONTROLS};
const HYPERLINK_START: &str = "\x1B]8;;";
const HYPERLINK_END: &str = "\x1B\x5C";
@ -352,7 +349,11 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
let mut display_hyperlink = false;
if self.options.embed_hyperlinks == EmbedHyperlinks::On {
if let Some(abs_path) = self.file.absolute_path().and_then(|p| p.as_os_str().to_str()) {
if let Some(abs_path) = self
.file
.absolute_path()
.and_then(|p| p.as_os_str().to_str())
{
let abs_path = utf8_percent_encode(abs_path, CONTROLS).to_string();
// On Windows, `std::fs::canonicalize` adds the Win32 File prefix, which we need to remove
@ -375,7 +376,9 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
);
if display_hyperlink {
bits.push(ANSIString::from(format!("{HYPERLINK_START}{HYPERLINK_END}")));
bits.push(ANSIString::from(format!(
"{HYPERLINK_START}{HYPERLINK_END}"
)));
}
bits
@ -394,6 +397,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
}
}
#[rustfmt::skip]
match self.file {
f if f.is_mount_point() => self.colours.mount_point(),
f if f.is_directory() => self.colours.directory(),
@ -419,10 +423,8 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
}
}
/// The set of colours that are needed to paint a file name.
pub trait Colours: FiletypeColours {
/// The style to paint the path of a symlinks target, up to but not
/// including the files name.
fn symlink_path(&self) -> Style;
@ -430,9 +432,9 @@ pub trait Colours: FiletypeColours {
/// The style to paint the arrow between a link and its target.
fn normal_arrow(&self) -> Style;
/// The style to paint the filenames of broken links in views that dont
/// show link targets, and the style to paint the *arrow* between the link
/// and its target in views that *do* show link targets.
/// The style to paint the filenames of broken links in views that dont
/// show link targets, and the style to paint the *arrow* between the link
/// and its target in views that *do* show link targets.
fn broken_symlink(&self) -> Style;
/// The style to paint the entire filename of a broken link.
@ -454,8 +456,7 @@ pub trait Colours: FiletypeColours {
fn colour_file(&self, file: &File<'_>) -> Style;
}
/// Generate a string made of `n` spaces.
fn spaces(width: u32) -> String {
(0 .. width).map(|_| ' ').collect()
(0..width).map(|_| ' ').collect()
}

View File

@ -2,13 +2,12 @@ use std::io::{self, Write};
use term_grid as tg;
use crate::fs::File;
use crate::fs::filter::FileFilter;
use crate::fs::File;
use crate::output::file_name::Options as FileStyle;
use crate::output::file_name::{ShowIcons, EmbedHyperlinks};
use crate::output::file_name::{EmbedHyperlinks, ShowIcons};
use crate::theme::Theme;
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct Options {
pub across: bool,
@ -16,12 +15,14 @@ pub struct Options {
impl Options {
pub fn direction(self) -> tg::Direction {
if self.across { tg::Direction::LeftToRight }
else { tg::Direction::TopToBottom }
if self.across {
tg::Direction::LeftToRight
} else {
tg::Direction::TopToBottom
}
}
}
pub struct Render<'a> {
pub files: Vec<File<'a>>,
pub theme: &'a Theme,
@ -34,8 +35,8 @@ pub struct Render<'a> {
impl<'a> Render<'a> {
pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
let mut grid = tg::Grid::new(tg::GridOptions {
direction: self.opts.direction(),
filling: tg::Filling::Spaces(2),
direction: self.opts.direction(),
filling: tg::Filling::Spaces(2),
});
grid.reserve(self.files.len());
@ -44,6 +45,7 @@ impl<'a> Render<'a> {
for file in &self.files {
let filename = self.file_style.for_file(file, self.theme);
let contents = filename.paint();
#[rustfmt::skip]
let width = match (filename.options.embed_hyperlinks, filename.options.show_icons) {
(EmbedHyperlinks::On, ShowIcons::On(spacing)) => filename.bare_width() + 1 + (spacing as usize),
(EmbedHyperlinks::On, ShowIcons::Off) => filename.bare_width(),
@ -51,7 +53,7 @@ impl<'a> Render<'a> {
};
grid.add(tg::Cell {
contents: contents.strings().to_string(),
contents: contents.strings().to_string(),
// with hyperlink escape sequences,
// the actual *contents.width() is larger than actually needed, so we take only the filename
width,
@ -60,8 +62,7 @@ impl<'a> Render<'a> {
if let Some(display) = grid.fit_into_width(self.console_width) {
write!(w, "{display}")
}
else {
} else {
// File names too long for a grid - drop down to just listing them!
// This isnt *quite* the same as the lines view, which also
// displays full link paths.

View File

@ -5,19 +5,20 @@ use std::io::{self, Write};
use ansiterm::ANSIStrings;
use term_grid as grid;
use crate::fs::{Dir, File};
use crate::fs::feature::git::GitCache;
use crate::fs::filter::FileFilter;
use crate::output::cell::{TextCell, DisplayWidth};
use crate::output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
use crate::fs::{Dir, File};
use crate::output::cell::{DisplayWidth, TextCell};
use crate::output::details::{
Options as DetailsOptions, Render as DetailsRender, Row as DetailsRow,
};
use crate::output::file_name::Options as FileStyle;
use crate::output::file_name::{ShowIcons, EmbedHyperlinks};
use crate::output::file_name::{EmbedHyperlinks, ShowIcons};
use crate::output::grid::Options as GridOptions;
use crate::output::table::{Table, Row as TableRow, Options as TableOptions};
use crate::output::tree::{TreeParams, TreeDepth};
use crate::output::table::{Options as TableOptions, Row as TableRow, Table};
use crate::output::tree::{TreeDepth, TreeParams};
use crate::theme::Theme;
#[derive(PartialEq, Eq, Debug)]
pub struct Options {
pub grid: GridOptions,
@ -31,7 +32,6 @@ impl Options {
}
}
/// The grid-details view can be configured to revert to just a details view
/// (with one column) if it wouldnt produce enough rows of output.
///
@ -41,7 +41,6 @@ impl Options {
/// larger directory listings.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum RowThreshold {
/// Only use grid-details view if it would result in at least this many
/// rows of output.
MinimumRows(usize),
@ -50,9 +49,7 @@ pub enum RowThreshold {
AlwaysGrid,
}
pub struct Render<'a> {
/// The directory thats being rendered here.
/// We need this to know which columns to put in the output.
pub dir: Option<&'a Dir>,
@ -91,7 +88,6 @@ pub struct Render<'a> {
}
impl<'a> Render<'a> {
/// Create a temporary Details render that gets used for the columns of
/// the grid-details render thats being generated.
///
@ -99,6 +95,7 @@ impl<'a> Render<'a> {
/// the table in *this* file, not in details: we only want to insert every
/// *n* files into each columns table, not all of them.
fn details_for_column(&self) -> DetailsRender<'a> {
#[rustfmt::skip]
DetailsRender {
dir: self.dir,
files: Vec::new(),
@ -117,6 +114,7 @@ impl<'a> Render<'a> {
/// when the user asked for a grid-details view but the terminal width is
/// not available, so we downgrade.
pub fn give_up(self) -> DetailsRender<'a> {
#[rustfmt::skip]
DetailsRender {
dir: self.dir,
files: self.files,
@ -136,27 +134,35 @@ impl<'a> Render<'a> {
pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
if let Some((grid, width)) = self.find_fitting_grid() {
write!(w, "{}", grid.fit_into_columns(width))
}
else {
} else {
self.give_up().render(w)
}
}
pub fn find_fitting_grid(&mut self) -> Option<(grid::Grid, grid::Width)> {
let options = self.details.table.as_ref().expect("Details table options not given!");
let options = self
.details
.table
.as_ref()
.expect("Details table options not given!");
let drender = self.details_for_column();
let (first_table, _) = self.make_table(options, &drender);
let rows = self.files.iter()
.map(|file| first_table.row_for_file(file, drender.show_xattr_hint(file)))
.collect::<Vec<_>>();
let rows = self
.files
.iter()
.map(|file| first_table.row_for_file(file, drender.show_xattr_hint(file)))
.collect::<Vec<_>>();
let file_names = self.files.iter()
let file_names = self
.files
.iter()
.map(|file| {
let filename = self.file_style.for_file(file, self.theme);
let contents = filename.paint();
#[rustfmt::skip]
let width = match (filename.options.embed_hyperlinks, filename.options.show_icons) {
(EmbedHyperlinks::On, ShowIcons::On(spacing)) => filename.bare_width() + 1 + (spacing as usize),
(EmbedHyperlinks::On, ShowIcons::Off) => filename.bare_width(),
@ -193,12 +199,20 @@ impl<'a> Render<'a> {
}
if !the_grid_fits || column_count == file_names.len() {
let last_column_count = if the_grid_fits { column_count } else { column_count - 1 };
let last_column_count = if the_grid_fits {
column_count
} else {
column_count - 1
};
// If weve figured out how many columns can fit in the users terminal,
// and it turns out there arent enough rows to make it worthwhile
// (according to EZA_GRID_ROWS), then just resort to the lines view.
if let RowThreshold::MinimumRows(thresh) = self.row_threshold {
if last_working_grid.fit_into_columns(last_column_count).row_count() < thresh {
if last_working_grid
.fit_into_columns(last_column_count)
.row_count()
< thresh
{
return None;
}
}
@ -210,7 +224,12 @@ impl<'a> Render<'a> {
None
}
fn make_table(&mut self, options: &'a TableOptions, drender: &DetailsRender<'_>) -> (Table<'a>, Vec<DetailsRow>) {
fn make_table(
&mut self,
options: &'a TableOptions,
drender: &DetailsRender<'_>,
) -> (Table<'a>, Vec<DetailsRow>) {
#[rustfmt::skip]
match (self.git, self.dir) {
(Some(g), Some(d)) => if ! g.has_anything_for(&d.path) { self.git = None },
(Some(g), None) => if ! self.files.iter().any(|f| g.has_anything_for(&f.path)) { self.git = None },
@ -229,9 +248,16 @@ impl<'a> Render<'a> {
(table, rows)
}
fn make_grid(&mut self, column_count: usize, options: &'a TableOptions, file_names: &[TextCell], rows: Vec<TableRow>, drender: &DetailsRender<'_>) -> grid::Grid {
fn make_grid(
&mut self,
column_count: usize,
options: &'a TableOptions,
file_names: &[TextCell],
rows: Vec<TableRow>,
drender: &DetailsRender<'_>,
) -> grid::Grid {
let mut tables = Vec::new();
for _ in 0 .. column_count {
for _ in 0..column_count {
tables.push(self.make_table(options, drender));
}
@ -245,52 +271,58 @@ impl<'a> Render<'a> {
for (i, (file_name, row)) in file_names.iter().zip(rows).enumerate() {
let index = if self.grid.across {
i % column_count
}
else {
i / original_height
};
i % column_count
} else {
i / original_height
};
let (ref mut table, ref mut rows) = tables[index];
table.add_widths(&row);
let details_row = drender.render_file(row, file_name.clone(), TreeParams::new(TreeDepth::root(), false));
let details_row = drender.render_file(
row,
file_name.clone(),
TreeParams::new(TreeDepth::root(), false),
);
rows.push(details_row);
}
let columns = tables
.into_iter()
.map(|(table, details_rows)| {
drender.iterate_with_table(table, details_rows)
.collect::<Vec<_>>()
})
drender
.iterate_with_table(table, details_rows)
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let direction = if self.grid.across { grid::Direction::LeftToRight }
else { grid::Direction::TopToBottom };
let direction = if self.grid.across {
grid::Direction::LeftToRight
} else {
grid::Direction::TopToBottom
};
let filling = grid::Filling::Spaces(4);
let mut grid = grid::Grid::new(grid::GridOptions { direction, filling });
if self.grid.across {
for row in 0 .. height {
for row in 0..height {
for column in &columns {
if row < column.len() {
let cell = grid::Cell {
contents: ANSIStrings(&column[row].contents).to_string(),
width: *column[row].width,
width: *column[row].width,
};
grid.add(cell);
}
}
}
}
else {
} else {
for column in &columns {
for cell in column {
let cell = grid::Cell {
contents: ANSIStrings(&cell.contents).to_string(),
width: *cell.width,
width: *cell.width,
};
grid.add(cell);
@ -302,7 +334,6 @@ impl<'a> Render<'a> {
}
}
fn divide_rounding_up(a: usize, b: usize) -> usize {
let mut result = a / b;

View File

@ -6,6 +6,7 @@ use crate::fs::File;
#[non_exhaustive]
struct Icons;
#[rustfmt::skip]
impl Icons {
const AUDIO: char = '\u{f001}'; // 
const BINARY: char = '\u{eae8}'; // 
@ -703,9 +704,11 @@ const EXTENSION_ICONS: Map<&'static str, char> = phf_map! {
/// - Attributes such as bold or underline should not be used to paint the
/// icon, as they can make it look weird.
pub fn iconify_style(style: Style) -> Style {
style.background.or(style.foreground)
.map(Style::from)
.unwrap_or_default()
style
.background
.or(style.foreground)
.map(Style::from)
.unwrap_or_default()
}
/// Lookup the icon for a file based on the file's name, if the entry is a

View File

@ -2,13 +2,12 @@ use std::io::{self, Write};
use ansiterm::ANSIStrings;
use crate::fs::File;
use crate::fs::filter::FileFilter;
use crate::fs::File;
use crate::output::cell::TextCellContents;
use crate::output::file_name::Options as FileStyle;
use crate::theme::Theme;
/// The lines view literally just displays each file, line-by-line.
pub struct Render<'a> {
pub files: Vec<File<'a>>,

View File

@ -1,4 +1,4 @@
pub use self::cell::{TextCell, TextCellContents, DisplayWidth};
pub use self::cell::{DisplayWidth, TextCell, TextCellContents};
pub use self::escape::escape;
pub mod details;
@ -15,7 +15,6 @@ mod cell;
mod escape;
mod tree;
/// The **view** contains all information about how to format output.
#[derive(Debug)]
pub struct View {
@ -25,7 +24,6 @@ pub struct View {
pub deref_links: bool,
}
/// The **mode** is the “type” of output.
#[derive(PartialEq, Eq, Debug)]
#[allow(clippy::large_enum_variant)]
@ -36,11 +34,9 @@ pub enum Mode {
Lines,
}
/// The width of the terminal requested by the user.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum TerminalWidth {
/// The user requested this specific number of columns.
Set(usize),
@ -54,6 +50,7 @@ impl TerminalWidth {
// terminal, but were only interested in stdout because its
// where the output goes.
#[rustfmt::skip]
match self {
Self::Set(width) => Some(width),
Self::Automatic => terminal_size::terminal_size().map(|(w, _)| w.0.into()),

View File

@ -3,28 +3,31 @@ use locale::Numeric as NumericLocale;
use number_prefix::Prefix;
use crate::fs::fields as f;
use crate::output::cell::{TextCell, DisplayWidth};
use crate::output::cell::{DisplayWidth, TextCell};
use crate::output::table::SizeFormat;
impl f::Blocksize {
pub fn render<C: Colours>(self, colours: &C, size_format: SizeFormat, numerics: &NumericLocale) -> TextCell {
pub fn render<C: Colours>(
self,
colours: &C,
size_format: SizeFormat,
numerics: &NumericLocale,
) -> TextCell {
use number_prefix::NumberPrefix;
let size = match self {
Self::Some(s) => s,
Self::None => return TextCell::blank(colours.no_blocksize()),
Self::Some(s) => s,
Self::None => return TextCell::blank(colours.no_blocksize()),
};
let result = match size_format {
SizeFormat::DecimalBytes => NumberPrefix::decimal(size as f64),
SizeFormat::BinaryBytes => NumberPrefix::binary(size as f64),
SizeFormat::JustBytes => {
SizeFormat::DecimalBytes => NumberPrefix::decimal(size as f64),
SizeFormat::BinaryBytes => NumberPrefix::binary(size as f64),
SizeFormat::JustBytes => {
// Use the binary prefix to select a style.
let prefix = match NumberPrefix::binary(size as f64) {
NumberPrefix::Standalone(_) => None,
NumberPrefix::Prefixed(p, _) => Some(p),
NumberPrefix::Standalone(_) => None,
NumberPrefix::Prefixed(p, _) => Some(p),
};
// But format the number directly using the locale.
@ -35,8 +38,10 @@ impl f::Blocksize {
};
let (prefix, n) = match result {
NumberPrefix::Standalone(b) => return TextCell::paint(colours.blocksize(None), numerics.format_int(b)),
NumberPrefix::Prefixed(p, n) => (p, n),
NumberPrefix::Standalone(b) => {
return TextCell::paint(colours.blocksize(None), numerics.format_int(b))
}
NumberPrefix::Prefixed(p, n) => (p, n),
};
let symbol = prefix.symbol();
@ -52,89 +57,106 @@ impl f::Blocksize {
contents: vec![
colours.blocksize(Some(prefix)).paint(number),
colours.unit(Some(prefix)).paint(symbol),
].into(),
]
.into(),
}
}
}
#[rustfmt::skip]
pub trait Colours {
fn blocksize(&self, prefix: Option<Prefix>) -> Style;
fn unit(&self, prefix: Option<Prefix>) -> Style;
fn no_blocksize(&self) -> Style;
}
#[cfg(test)]
pub mod test {
use ansiterm::Style;
use ansiterm::Colour::*;
use ansiterm::Style;
use super::Colours;
use crate::output::cell::{TextCell, DisplayWidth};
use crate::output::table::SizeFormat;
use crate::fs::fields as f;
use crate::output::cell::{DisplayWidth, TextCell};
use crate::output::table::SizeFormat;
use locale::Numeric as NumericLocale;
use number_prefix::Prefix;
struct TestColours;
#[rustfmt::skip]
impl Colours for TestColours {
fn blocksize(&self, _prefix: Option<Prefix>) -> Style { Fixed(66).normal() }
fn unit(&self, _prefix: Option<Prefix>) -> Style { Fixed(77).bold() }
fn no_blocksize(&self) -> Style { Black.italic() }
}
#[test]
fn directory() {
let directory = f::Blocksize::None;
let expected = TextCell::blank(Black.italic());
assert_eq!(expected, directory.render(&TestColours, SizeFormat::JustBytes, &NumericLocale::english()))
assert_eq!(
expected,
directory.render(
&TestColours,
SizeFormat::JustBytes,
&NumericLocale::english()
)
)
}
#[test]
fn file_decimal() {
let directory = f::Blocksize::Some(2_100_000);
let expected = TextCell {
width: DisplayWidth::from(4),
contents: vec![
Fixed(66).paint("2.1"),
Fixed(77).bold().paint("M"),
].into(),
contents: vec![Fixed(66).paint("2.1"), Fixed(77).bold().paint("M")].into(),
};
assert_eq!(expected, directory.render(&TestColours, SizeFormat::DecimalBytes, &NumericLocale::english()))
assert_eq!(
expected,
directory.render(
&TestColours,
SizeFormat::DecimalBytes,
&NumericLocale::english()
)
)
}
#[test]
fn file_binary() {
let directory = f::Blocksize::Some(1_048_576);
let expected = TextCell {
width: DisplayWidth::from(5),
contents: vec![
Fixed(66).paint("1.0"),
Fixed(77).bold().paint("Mi"),
].into(),
contents: vec![Fixed(66).paint("1.0"), Fixed(77).bold().paint("Mi")].into(),
};
assert_eq!(expected, directory.render(&TestColours, SizeFormat::BinaryBytes, &NumericLocale::english()))
assert_eq!(
expected,
directory.render(
&TestColours,
SizeFormat::BinaryBytes,
&NumericLocale::english()
)
)
}
#[test]
fn file_bytes() {
let directory = f::Blocksize::Some(1_048_576);
let expected = TextCell {
width: DisplayWidth::from(9),
contents: vec![
Fixed(66).paint("1,048,576"),
].into(),
contents: vec![Fixed(66).paint("1,048,576")].into(),
};
assert_eq!(expected, directory.render(&TestColours, SizeFormat::JustBytes, &NumericLocale::english()))
assert_eq!(
expected,
directory.render(
&TestColours,
SizeFormat::JustBytes,
&NumericLocale::english()
)
)
}
}

View File

@ -2,9 +2,9 @@ use ansiterm::{ANSIString, Style};
use crate::fs::fields as f;
impl f::Type {
pub fn render<C: Colours>(self, colours: &C) -> ANSIString<'static> {
#[rustfmt::skip]
match self {
Self::File => colours.normal().paint("."),
Self::Directory => colours.directory().paint("d"),
@ -18,7 +18,6 @@ impl f::Type {
}
}
pub trait Colours {
fn normal(&self) -> Style;
fn directory(&self) -> Style;

View File

@ -1,23 +1,20 @@
use ansiterm::{ANSIString, Style};
use crate::output::cell::{TextCell, DisplayWidth};
use crate::fs::fields as f;
use crate::output::cell::{DisplayWidth, TextCell};
impl f::Git {
pub fn render(self, colours: &dyn Colours) -> TextCell {
TextCell {
width: DisplayWidth::from(2),
contents: vec![
self.staged.render(colours),
self.unstaged.render(colours),
].into(),
contents: vec![self.staged.render(colours), self.unstaged.render(colours)].into(),
}
}
}
impl f::GitStatus {
fn render(self, colours: &dyn Colours) -> ANSIString<'static> {
#[rustfmt::skip]
match self {
Self::NotModified => colours.not_modified().paint("-"),
Self::New => colours.new().paint("N"),
@ -35,7 +32,7 @@ pub trait Colours {
fn not_modified(&self) -> Style;
// FIXME: this amount of allows needed to keep clippy happy should be enough
// of an argument that new needs to be renamed.
#[allow(clippy::new_ret_no_self,clippy::wrong_self_convention)]
#[allow(clippy::new_ret_no_self, clippy::wrong_self_convention)]
fn new(&self) -> Style;
fn modified(&self) -> Style;
fn deleted(&self) -> Style;
@ -45,7 +42,6 @@ pub trait Colours {
fn conflicted(&self) -> Style;
}
impl f::SubdirGitRepo {
pub fn render(self, colours: &dyn RepoColours) -> TextCell {
let branch_name = match self.branch {
@ -92,63 +88,69 @@ pub trait RepoColours {
fn git_dirty(&self) -> Style;
}
#[cfg(test)]
pub mod test {
use super::Colours;
use crate::output::cell::{TextCell, DisplayWidth};
use crate::fs::fields as f;
use crate::output::cell::{DisplayWidth, TextCell};
use ansiterm::Colour::*;
use ansiterm::Style;
struct TestColours;
impl Colours for TestColours {
fn not_modified(&self) -> Style { Fixed(90).normal() }
fn new(&self) -> Style { Fixed(91).normal() }
fn modified(&self) -> Style { Fixed(92).normal() }
fn deleted(&self) -> Style { Fixed(93).normal() }
fn renamed(&self) -> Style { Fixed(94).normal() }
fn type_change(&self) -> Style { Fixed(95).normal() }
fn ignored(&self) -> Style { Fixed(96).normal() }
fn conflicted(&self) -> Style { Fixed(97).normal() }
fn not_modified(&self) -> Style {
Fixed(90).normal()
}
fn new(&self) -> Style {
Fixed(91).normal()
}
fn modified(&self) -> Style {
Fixed(92).normal()
}
fn deleted(&self) -> Style {
Fixed(93).normal()
}
fn renamed(&self) -> Style {
Fixed(94).normal()
}
fn type_change(&self) -> Style {
Fixed(95).normal()
}
fn ignored(&self) -> Style {
Fixed(96).normal()
}
fn conflicted(&self) -> Style {
Fixed(97).normal()
}
}
#[test]
fn git_blank() {
let stati = f::Git {
staged: f::GitStatus::NotModified,
staged: f::GitStatus::NotModified,
unstaged: f::GitStatus::NotModified,
};
let expected = TextCell {
width: DisplayWidth::from(2),
contents: vec![
Fixed(90).paint("-"),
Fixed(90).paint("-"),
].into(),
contents: vec![Fixed(90).paint("-"), Fixed(90).paint("-")].into(),
};
assert_eq!(expected, stati.render(&TestColours))
}
#[test]
fn git_new_changed() {
let stati = f::Git {
staged: f::GitStatus::New,
staged: f::GitStatus::New,
unstaged: f::GitStatus::Modified,
};
let expected = TextCell {
width: DisplayWidth::from(2),
contents: vec![
Fixed(91).paint("N"),
Fixed(92).paint("M"),
].into(),
contents: vec![Fixed(91).paint("N"), Fixed(92).paint("M")].into(),
};
assert_eq!(expected, stati.render(&TestColours))

View File

@ -1,16 +1,26 @@
use ansiterm::Style;
use uzers::{Users, Groups};
use uzers::{Groups, Users};
use crate::fs::fields as f;
use crate::output::cell::TextCell;
use crate::output::table::UserFormat;
pub trait Render{
fn render<C: Colours, U: Users+Groups>(self, colours: &C, users: &U, format: UserFormat) -> TextCell;
pub trait Render {
fn render<C: Colours, U: Users + Groups>(
self,
colours: &C,
users: &U,
format: UserFormat,
) -> TextCell;
}
impl Render for Option<f::Group> {
fn render<C: Colours, U: Users+Groups>(self, colours: &C, users: &U, format: UserFormat) -> TextCell {
fn render<C: Colours, U: Users + Groups>(
self,
colours: &C,
users: &U,
format: UserFormat,
) -> TextCell {
use uzers::os::unix::GroupExt;
let mut style = colours.not_yours();
@ -18,17 +28,15 @@ impl Render for Option<f::Group> {
let group = match self {
Some(g) => match users.get_group_by_gid(g.0) {
Some(g) => (*g).clone(),
None => return TextCell::paint(style, g.0.to_string()),
None => return TextCell::paint(style, g.0.to_string()),
},
None => return TextCell::blank(colours.no_group()),
};
let current_uid = users.get_current_uid();
if let Some(current_user) = users.get_user_by_uid(current_uid) {
if current_user.primary_group_id() == group.gid()
|| group.members().iter().any(|u| u == current_user.name())
|| group.members().iter().any(|u| u == current_user.name())
{
style = colours.yours();
}
@ -43,14 +51,12 @@ impl Render for Option<f::Group> {
}
}
pub trait Colours {
fn yours(&self) -> Style;
fn not_yours(&self) -> Style;
fn no_group(&self) -> Style;
}
#[cfg(test)]
#[allow(unused_results)]
pub mod test {
@ -59,22 +65,21 @@ pub mod test {
use crate::output::cell::TextCell;
use crate::output::table::UserFormat;
use uzers::{User, Group};
use uzers::mock::MockUsers;
use uzers::os::unix::GroupExt;
use ansiterm::Colour::*;
use ansiterm::Style;
use uzers::mock::MockUsers;
use uzers::os::unix::GroupExt;
use uzers::{Group, User};
struct TestColours;
#[rustfmt::skip]
impl Colours for TestColours {
fn yours(&self) -> Style { Fixed(80).normal() }
fn not_yours(&self) -> Style { Fixed(81).normal() }
fn no_group(&self) -> Style { Black.italic() }
}
#[test]
fn named() {
let mut users = MockUsers::with_current_uid(1000);
@ -82,21 +87,32 @@ pub mod test {
let group = Some(f::Group(100));
let expected = TextCell::paint_str(Fixed(81).normal(), "folk");
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name));
assert_eq!(
expected,
group.render(&TestColours, &users, UserFormat::Name)
);
let expected = TextCell::paint_str(Fixed(81).normal(), "100");
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Numeric));
assert_eq!(
expected,
group.render(&TestColours, &users, UserFormat::Numeric)
);
}
#[test]
fn unnamed() {
let users = MockUsers::with_current_uid(1000);
let group = Some(f::Group(100));
let expected = TextCell::paint_str(Fixed(81).normal(), "100");
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name));
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Numeric));
assert_eq!(
expected,
group.render(&TestColours, &users, UserFormat::Name)
);
assert_eq!(
expected,
group.render(&TestColours, &users, UserFormat::Numeric)
);
}
#[test]
@ -107,7 +123,10 @@ pub mod test {
let group = Some(f::Group(100));
let expected = TextCell::paint_str(Fixed(80).normal(), "folk");
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name))
assert_eq!(
expected,
group.render(&TestColours, &users, UserFormat::Name)
)
}
#[test]
@ -120,13 +139,23 @@ pub mod test {
let group = Some(f::Group(100));
let expected = TextCell::paint_str(Fixed(80).normal(), "folk");
assert_eq!(expected, group.render(&TestColours, &users, UserFormat::Name))
assert_eq!(
expected,
group.render(&TestColours, &users, UserFormat::Name)
)
}
#[test]
fn overflow() {
let group = Some(f::Group(2_147_483_648));
let expected = TextCell::paint_str(Fixed(81).normal(), "2147483648");
assert_eq!(expected, group.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
assert_eq!(
expected,
group.render(
&TestColours,
&MockUsers::with_current_uid(0),
UserFormat::Numeric
)
);
}
}

View File

@ -3,22 +3,19 @@ use ansiterm::Style;
use crate::fs::fields as f;
use crate::output::cell::TextCell;
impl f::Inode {
pub fn render(self, style: Style) -> TextCell {
TextCell::paint(style, self.0.to_string())
}
}
#[cfg(test)]
pub mod test {
use crate::output::cell::TextCell;
use crate::fs::fields as f;
use crate::output::cell::TextCell;
use ansiterm::Colour::*;
#[test]
fn blocklessness() {
let io = f::Inode(1_414_213);

View File

@ -10,87 +10,99 @@ use crate::output::cell::TextCell;
#[cfg(unix)]
impl f::Links {
pub fn render<C: Colours>(&self, colours: &C, numeric: &NumericLocale) -> TextCell {
let style = if self.multiple { colours.multi_link_file() }
else { colours.normal() };
let style = if self.multiple {
colours.multi_link_file()
} else {
colours.normal()
};
TextCell::paint(style, numeric.format_int(self.count))
}
}
pub trait Colours {
fn normal(&self) -> Style;
fn multi_link_file(&self) -> Style;
}
#[cfg(test)]
pub mod test {
use super::Colours;
#[cfg(unix)]
use crate::output::cell::{TextCell, DisplayWidth};
#[cfg(unix)]
use crate::fs::fields as f;
#[cfg(unix)]
use crate::output::cell::{DisplayWidth, TextCell};
use ansiterm::Colour::*;
use ansiterm::Style;
#[cfg(unix)]
use locale;
struct TestColours;
impl Colours for TestColours {
fn normal(&self) -> Style { Blue.normal() }
fn multi_link_file(&self) -> Style { Blue.on(Red) }
fn normal(&self) -> Style {
Blue.normal()
}
fn multi_link_file(&self) -> Style {
Blue.on(Red)
}
}
#[test]
#[cfg(unix)]
fn regular_file() {
let stati = f::Links {
count: 1,
count: 1,
multiple: false,
};
let expected = TextCell {
width: DisplayWidth::from(1),
contents: vec![ Blue.paint("1") ].into(),
contents: vec![Blue.paint("1")].into(),
};
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()));
assert_eq!(
expected,
stati.render(&TestColours, &locale::Numeric::english())
);
}
#[test]
#[cfg(unix)]
fn regular_directory() {
let stati = f::Links {
count: 3005,
count: 3005,
multiple: false,
};
let expected = TextCell {
width: DisplayWidth::from(5),
contents: vec![ Blue.paint("3,005") ].into(),
contents: vec![Blue.paint("3,005")].into(),
};
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()));
assert_eq!(
expected,
stati.render(&TestColours, &locale::Numeric::english())
);
}
#[test]
#[cfg(unix)]
fn popular_file() {
let stati = f::Links {
count: 3005,
count: 3005,
multiple: true,
};
let expected = TextCell {
width: DisplayWidth::from(5),
contents: vec![ Blue.on(Red).paint("3,005") ].into(),
contents: vec![Blue.on(Red).paint("3,005")].into(),
};
assert_eq!(expected, stati.render(&TestColours, &locale::Numeric::english()));
assert_eq!(
expected,
stati.render(&TestColours, &locale::Numeric::english())
);
}
}

View File

@ -9,6 +9,7 @@ pub trait Render {
impl Render for Option<f::OctalPermissions> {
fn render(&self, style: Style) -> TextCell {
#[rustfmt::skip]
match self {
Some(p) => {
let perm = &p.permissions;
@ -30,25 +31,32 @@ impl f::OctalPermissions {
}
}
#[cfg(test)]
pub mod test {
use super::Render;
use crate::output::cell::TextCell;
use crate::fs::fields as f;
use crate::output::cell::TextCell;
use ansiterm::Colour::*;
#[test]
fn normal_folder() {
let bits = f::Permissions {
user_read: true, user_write: true, user_execute: true, setuid: false,
group_read: true, group_write: false, group_execute: true, setgid: false,
other_read: true, other_write: false, other_execute: true, sticky: false,
user_read: true,
user_write: true,
user_execute: true,
setuid: false,
group_read: true,
group_write: false,
group_execute: true,
setgid: false,
other_read: true,
other_write: false,
other_execute: true,
sticky: false,
};
let octal = Some(f::OctalPermissions{ permissions: bits });
let octal = Some(f::OctalPermissions { permissions: bits });
let expected = TextCell::paint_str(Purple.bold(), "0755");
assert_eq!(expected, octal.render(Purple.bold()));
@ -57,12 +65,21 @@ pub mod test {
#[test]
fn normal_file() {
let bits = f::Permissions {
user_read: true, user_write: true, user_execute: false, setuid: false,
group_read: true, group_write: false, group_execute: false, setgid: false,
other_read: true, other_write: false, other_execute: false, sticky: false,
user_read: true,
user_write: true,
user_execute: false,
setuid: false,
group_read: true,
group_write: false,
group_execute: false,
setgid: false,
other_read: true,
other_write: false,
other_execute: false,
sticky: false,
};
let octal = Some(f::OctalPermissions{ permissions: bits });
let octal = Some(f::OctalPermissions { permissions: bits });
let expected = TextCell::paint_str(Purple.bold(), "0644");
assert_eq!(expected, octal.render(Purple.bold()));
@ -71,12 +88,21 @@ pub mod test {
#[test]
fn secret_file() {
let bits = f::Permissions {
user_read: true, user_write: true, user_execute: false, setuid: false,
group_read: false, group_write: false, group_execute: false, setgid: false,
other_read: false, other_write: false, other_execute: false, sticky: false,
user_read: true,
user_write: true,
user_execute: false,
setuid: false,
group_read: false,
group_write: false,
group_execute: false,
setgid: false,
other_read: false,
other_write: false,
other_execute: false,
sticky: false,
};
let octal = Some(f::OctalPermissions{ permissions: bits });
let octal = Some(f::OctalPermissions { permissions: bits });
let expected = TextCell::paint_str(Purple.bold(), "0600");
assert_eq!(expected, octal.render(Purple.bold()));
@ -85,27 +111,44 @@ pub mod test {
#[test]
fn sticky1() {
let bits = f::Permissions {
user_read: true, user_write: true, user_execute: true, setuid: true,
group_read: true, group_write: true, group_execute: true, setgid: false,
other_read: true, other_write: true, other_execute: true, sticky: false,
user_read: true,
user_write: true,
user_execute: true,
setuid: true,
group_read: true,
group_write: true,
group_execute: true,
setgid: false,
other_read: true,
other_write: true,
other_execute: true,
sticky: false,
};
let octal = Some(f::OctalPermissions{ permissions: bits });
let octal = Some(f::OctalPermissions { permissions: bits });
let expected = TextCell::paint_str(Purple.bold(), "4777");
assert_eq!(expected, octal.render(Purple.bold()));
}
#[test]
fn sticky2() {
let bits = f::Permissions {
user_read: true, user_write: true, user_execute: true, setuid: false,
group_read: true, group_write: true, group_execute: true, setgid: true,
other_read: true, other_write: true, other_execute: true, sticky: false,
user_read: true,
user_write: true,
user_execute: true,
setuid: false,
group_read: true,
group_write: true,
group_execute: true,
setgid: true,
other_read: true,
other_write: true,
other_execute: true,
sticky: false,
};
let octal = Some(f::OctalPermissions{ permissions: bits });
let octal = Some(f::OctalPermissions { permissions: bits });
let expected = TextCell::paint_str(Purple.bold(), "2777");
assert_eq!(expected, octal.render(Purple.bold()));
@ -114,12 +157,21 @@ pub mod test {
#[test]
fn sticky3() {
let bits = f::Permissions {
user_read: true, user_write: true, user_execute: true, setuid: false,
group_read: true, group_write: true, group_execute: true, setgid: false,
other_read: true, other_write: true, other_execute: true, sticky: true,
user_read: true,
user_write: true,
user_execute: true,
setuid: false,
group_read: true,
group_write: true,
group_execute: true,
setgid: false,
other_read: true,
other_write: true,
other_execute: true,
sticky: true,
};
let octal = Some(f::OctalPermissions{ permissions: bits });
let octal = Some(f::OctalPermissions { permissions: bits });
let expected = TextCell::paint_str(Purple.bold(), "1777");
assert_eq!(expected, octal.render(Purple.bold()));

View File

@ -3,38 +3,38 @@ use std::iter;
use ansiterm::{ANSIString, Style};
use crate::fs::fields as f;
use crate::output::cell::{TextCell, DisplayWidth};
use crate::output::cell::{DisplayWidth, TextCell};
use crate::output::render::FiletypeColours;
pub trait PermissionsPlusRender {
fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell;
fn render<C: Colours + FiletypeColours>(&self, colours: &C) -> TextCell;
}
impl PermissionsPlusRender for Option<f::PermissionsPlus> {
#[cfg(unix)]
fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
fn render<C: Colours + FiletypeColours>(&self, colours: &C) -> TextCell {
match self {
Some(p) => {
let mut chars = vec![ p.file_type.render(colours) ];
let mut chars = vec![p.file_type.render(colours)];
let permissions = p.permissions;
chars.extend(Some(permissions).render(colours, p.file_type.is_regular_file()));
if p.xattrs {
chars.push(colours.attribute().paint("@"));
chars.push(colours.attribute().paint("@"));
}
// As these are all ASCII characters, we can guarantee that theyre
// all going to be one character wide, and dont need to compute the
// cells display width.
TextCell {
width: DisplayWidth::from(chars.len()),
width: DisplayWidth::from(chars.len()),
contents: chars.into(),
}
},
}
None => {
let chars: Vec<_> = iter::repeat(colours.dash().paint("-")).take(10).collect();
TextCell {
width: DisplayWidth::from(chars.len()),
width: DisplayWidth::from(chars.len()),
contents: chars.into(),
}
}
@ -42,23 +42,21 @@ impl PermissionsPlusRender for Option<f::PermissionsPlus> {
}
#[cfg(windows)]
fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> TextCell {
fn render<C: Colours + FiletypeColours>(&self, colours: &C) -> TextCell {
match self {
Some(p) => {
let mut chars = vec![ p.attributes.render_type(colours) ];
let mut chars = vec![p.attributes.render_type(colours)];
chars.extend(p.attributes.render(colours));
TextCell {
width: DisplayWidth::from(chars.len()),
width: DisplayWidth::from(chars.len()),
contents: chars.into(),
}
},
None => {
TextCell {
width: DisplayWidth::from(0),
contents: vec![].into(),
}
}
}
None => TextCell {
width: DisplayWidth::from(0),
contents: vec![].into(),
},
}
}
}
@ -72,10 +70,14 @@ impl RenderPermissions for Option<f::Permissions> {
match self {
Some(p) => {
let bit = |bit, chr: &'static str, style: Style| {
if bit { style.paint(chr) }
else { colours.dash().paint("-") }
if bit {
style.paint(chr)
} else {
colours.dash().paint("-")
}
};
#[rustfmt::skip]
vec![
bit(p.user_read, "r", colours.user_read()),
bit(p.user_write, "w", colours.user_write()),
@ -87,16 +89,19 @@ impl RenderPermissions for Option<f::Permissions> {
bit(p.other_write, "w", colours.other_write()),
p.other_execute_bit(colours)
]
},
None => {
iter::repeat(colours.dash().paint("-")).take(9).collect()
}
None => iter::repeat(colours.dash().paint("-")).take(9).collect(),
}
}
}
impl f::Permissions {
fn user_execute_bit<C: Colours>(&self, colours: &C, is_regular_file: bool) -> ANSIString<'static> {
fn user_execute_bit<C: Colours>(
&self,
colours: &C,
is_regular_file: bool,
) -> ANSIString<'static> {
#[rustfmt::skip]
match (self.user_execute, self.setuid, is_regular_file) {
(false, false, _) => colours.dash().paint("-"),
(true, false, false) => colours.user_execute_other().paint("x"),
@ -108,6 +113,7 @@ impl f::Permissions {
}
fn group_execute_bit<C: Colours>(&self, colours: &C) -> ANSIString<'static> {
#[rustfmt::skip]
match (self.group_execute, self.setgid) {
(false, false) => colours.dash().paint("-"),
(true, false) => colours.group_execute().paint("x"),
@ -117,6 +123,7 @@ impl f::Permissions {
}
fn other_execute_bit<C: Colours>(&self, colours: &C) -> ANSIString<'static> {
#[rustfmt::skip]
match (self.other_execute, self.sticky) {
(false, false) => colours.dash().paint("-"),
(true, false) => colours.other_execute().paint("x"),
@ -128,12 +135,16 @@ impl f::Permissions {
#[cfg(windows)]
impl f::Attributes {
pub fn render<C: Colours+FiletypeColours>(self, colours: &C) -> Vec<ANSIString<'static>> {
pub fn render<C: Colours + FiletypeColours>(self, colours: &C) -> Vec<ANSIString<'static>> {
let bit = |bit, chr: &'static str, style: Style| {
if bit { style.paint(chr) }
else { colours.dash().paint("-") }
if bit {
style.paint(chr)
} else {
colours.dash().paint("-")
}
};
#[rustfmt::skip]
vec![
bit(self.archive, "a", colours.normal()),
bit(self.readonly, "r", colours.user_read()),
@ -142,12 +153,11 @@ impl f::Attributes {
]
}
pub fn render_type<C: Colours+FiletypeColours>(self, colours: &C) -> ANSIString<'static> {
pub fn render_type<C: Colours + FiletypeColours>(self, colours: &C) -> ANSIString<'static> {
if self.reparse_point {
return colours.pipe().paint("l")
}
else if self.directory {
return colours.directory().paint("d")
return colours.pipe().paint("l");
} else if self.directory {
return colours.directory().paint("d");
}
colours.dash().paint("-")
}
@ -175,20 +185,19 @@ pub trait Colours {
fn attribute(&self) -> Style;
}
#[cfg(test)]
#[allow(unused_results)]
pub mod test {
use super::{Colours, RenderPermissions};
use crate::output::cell::TextCellContents;
use crate::fs::fields as f;
use crate::output::cell::TextCellContents;
use ansiterm::Colour::*;
use ansiterm::Style;
struct TestColours;
#[rustfmt::skip]
impl Colours for TestColours {
fn dash(&self) -> Style { Fixed(11).normal() }
fn user_read(&self) -> Style { Fixed(101).normal() }
@ -206,73 +215,129 @@ pub mod test {
fn attribute(&self) -> Style { Fixed(112).normal() }
}
#[test]
fn negate() {
let bits = Some(f::Permissions {
user_read: false, user_write: false, user_execute: false, setuid: false,
group_read: false, group_write: false, group_execute: false, setgid: false,
other_read: false, other_write: false, other_execute: false, sticky: false,
user_read: false,
user_write: false,
user_execute: false,
setuid: false,
group_read: false,
group_write: false,
group_execute: false,
setgid: false,
other_read: false,
other_write: false,
other_execute: false,
sticky: false,
});
let expected = TextCellContents::from(vec![
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(11).paint("-"),
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(11).paint("-"),
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
]);
assert_eq!(expected, bits.render(&TestColours, false).into())
}
#[test]
fn affirm() {
let bits = Some(f::Permissions {
user_read: true, user_write: true, user_execute: true, setuid: false,
group_read: true, group_write: true, group_execute: true, setgid: false,
other_read: true, other_write: true, other_execute: true, sticky: false,
user_read: true,
user_write: true,
user_execute: true,
setuid: false,
group_read: true,
group_write: true,
group_execute: true,
setgid: false,
other_read: true,
other_write: true,
other_execute: true,
sticky: false,
});
let expected = TextCellContents::from(vec![
Fixed(101).paint("r"), Fixed(102).paint("w"), Fixed(103).paint("x"),
Fixed(104).paint("r"), Fixed(105).paint("w"), Fixed(106).paint("x"),
Fixed(107).paint("r"), Fixed(108).paint("w"), Fixed(109).paint("x"),
Fixed(101).paint("r"),
Fixed(102).paint("w"),
Fixed(103).paint("x"),
Fixed(104).paint("r"),
Fixed(105).paint("w"),
Fixed(106).paint("x"),
Fixed(107).paint("r"),
Fixed(108).paint("w"),
Fixed(109).paint("x"),
]);
assert_eq!(expected, bits.render(&TestColours, true).into())
}
#[test]
fn specials() {
let bits = Some(f::Permissions {
user_read: false, user_write: false, user_execute: true, setuid: true,
group_read: false, group_write: false, group_execute: true, setgid: true,
other_read: false, other_write: false, other_execute: true, sticky: true,
user_read: false,
user_write: false,
user_execute: true,
setuid: true,
group_read: false,
group_write: false,
group_execute: true,
setgid: true,
other_read: false,
other_write: false,
other_execute: true,
sticky: true,
});
let expected = TextCellContents::from(vec![
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(110).paint("s"),
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("s"),
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("t"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(110).paint("s"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(111).paint("s"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(111).paint("t"),
]);
assert_eq!(expected, bits.render(&TestColours, true).into())
}
#[test]
fn extra_specials() {
let bits = Some(f::Permissions {
user_read: false, user_write: false, user_execute: false, setuid: true,
group_read: false, group_write: false, group_execute: false, setgid: true,
other_read: false, other_write: false, other_execute: false, sticky: true,
user_read: false,
user_write: false,
user_execute: false,
setuid: true,
group_read: false,
group_write: false,
group_execute: false,
setgid: true,
other_read: false,
other_write: false,
other_execute: false,
sticky: true,
});
let expected = TextCellContents::from(vec![
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("S"),
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("S"),
Fixed(11).paint("-"), Fixed(11).paint("-"), Fixed(111).paint("T"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(111).paint("S"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(111).paint("S"),
Fixed(11).paint("-"),
Fixed(11).paint("-"),
Fixed(111).paint("T"),
]);
assert_eq!(expected, bits.render(&TestColours, true).into())

View File

@ -1,15 +1,12 @@
use ansiterm::Style;
use crate::fs::fields as f;
use crate::output::cell::{TextCell, DisplayWidth};
use crate::output::cell::{DisplayWidth, TextCell};
impl f::SecurityContext<'_> {
pub fn render<C: Colours>(&self, colours: &C) -> TextCell {
match &self.context {
f::SecurityContextType::None => {
TextCell::paint_str(colours.none(), "?")
}
f::SecurityContextType::None => TextCell::paint_str(colours.none(), "?"),
f::SecurityContextType::SELinux(context) => {
let mut chars = Vec::with_capacity(7);
@ -18,7 +15,7 @@ impl f::SecurityContext<'_> {
0 => colours.selinux_user(),
1 => colours.selinux_role(),
2 => colours.selinux_type(),
_ => colours.selinux_range()
_ => colours.selinux_range(),
};
if i > 0 {
chars.push(colours.selinux_colon().paint(":"));
@ -28,15 +25,16 @@ impl f::SecurityContext<'_> {
TextCell {
contents: chars.into(),
width: DisplayWidth::from(context.len())
width: DisplayWidth::from(context.len()),
}
}
}
}
}
#[rustfmt::skip]
pub trait Colours {
fn none(&self) -> Style;
fn none(&self) -> Style;
fn selinux_colon(&self) -> Style;
fn selinux_user(&self) -> Style;
fn selinux_role(&self) -> Style;

View File

@ -3,20 +3,25 @@ use locale::Numeric as NumericLocale;
use number_prefix::Prefix;
use crate::fs::fields as f;
use crate::output::cell::{TextCell, DisplayWidth};
use crate::output::cell::{DisplayWidth, TextCell};
use crate::output::table::SizeFormat;
impl f::Size {
pub fn render<C: Colours>(self, colours: &C, size_format: SizeFormat, numerics: &NumericLocale) -> TextCell {
pub fn render<C: Colours>(
self,
colours: &C,
size_format: SizeFormat,
numerics: &NumericLocale,
) -> TextCell {
use number_prefix::NumberPrefix;
let size = match self {
Self::Some(s) => s,
Self::None => return TextCell::blank(colours.no_size()),
Self::DeviceIDs(ref ids) => return ids.render(colours),
Self::Some(s) => s,
Self::None => return TextCell::blank(colours.no_size()),
Self::DeviceIDs(ref ids) => return ids.render(colours),
};
#[rustfmt::skip]
let result = match size_format {
SizeFormat::DecimalBytes => NumberPrefix::decimal(size as f64),
SizeFormat::BinaryBytes => NumberPrefix::binary(size as f64),
@ -35,6 +40,7 @@ impl f::Size {
}
};
#[rustfmt::skip]
let (prefix, n) = match result {
NumberPrefix::Standalone(b) => return TextCell::paint(colours.size(None), numerics.format_int(b)),
NumberPrefix::Prefixed(p, n) => (p, n),
@ -53,12 +59,12 @@ impl f::Size {
contents: vec![
colours.size(Some(prefix)).paint(number),
colours.unit(Some(prefix)).paint(symbol),
].into(),
]
.into(),
}
}
}
impl f::DeviceIDs {
fn render<C: Colours>(self, colours: &C) -> TextCell {
let major = self.major.to_string();
@ -70,12 +76,12 @@ impl f::DeviceIDs {
colours.major().paint(major),
colours.comma().paint(","),
colours.minor().paint(minor),
].into(),
]
.into(),
}
}
}
pub trait Colours {
fn size(&self, prefix: Option<Prefix>) -> Style;
fn unit(&self, prefix: Option<Prefix>) -> Style;
@ -86,97 +92,122 @@ pub trait Colours {
fn minor(&self) -> Style;
}
#[cfg(test)]
pub mod test {
use super::Colours;
use crate::output::cell::{TextCell, DisplayWidth};
use crate::output::table::SizeFormat;
use crate::fs::fields as f;
use crate::output::cell::{DisplayWidth, TextCell};
use crate::output::table::SizeFormat;
use locale::Numeric as NumericLocale;
use ansiterm::Colour::*;
use ansiterm::Style;
use locale::Numeric as NumericLocale;
use number_prefix::Prefix;
struct TestColours;
#[rustfmt::skip]
impl Colours for TestColours {
fn size(&self, _prefix: Option<Prefix>) -> Style { Fixed(66).normal() }
fn unit(&self, _prefix: Option<Prefix>) -> Style { Fixed(77).bold() }
fn no_size(&self) -> Style { Black.italic() }
fn no_size(&self) -> Style { Black.italic() }
fn major(&self) -> Style { Blue.on(Red) }
fn comma(&self) -> Style { Green.italic() }
fn minor(&self) -> Style { Cyan.on(Yellow) }
}
#[test]
fn directory() {
let directory = f::Size::None;
let expected = TextCell::blank(Black.italic());
assert_eq!(expected, directory.render(&TestColours, SizeFormat::JustBytes, &NumericLocale::english()))
assert_eq!(
expected,
directory.render(
&TestColours,
SizeFormat::JustBytes,
&NumericLocale::english()
)
)
}
#[test]
fn file_decimal() {
let directory = f::Size::Some(2_100_000);
let expected = TextCell {
width: DisplayWidth::from(4),
contents: vec![
Fixed(66).paint("2.1"),
Fixed(77).bold().paint("M"),
].into(),
contents: vec![Fixed(66).paint("2.1"), Fixed(77).bold().paint("M")].into(),
};
assert_eq!(expected, directory.render(&TestColours, SizeFormat::DecimalBytes, &NumericLocale::english()))
assert_eq!(
expected,
directory.render(
&TestColours,
SizeFormat::DecimalBytes,
&NumericLocale::english()
)
)
}
#[test]
fn file_binary() {
let directory = f::Size::Some(1_048_576);
let expected = TextCell {
width: DisplayWidth::from(5),
contents: vec![
Fixed(66).paint("1.0"),
Fixed(77).bold().paint("Mi"),
].into(),
contents: vec![Fixed(66).paint("1.0"), Fixed(77).bold().paint("Mi")].into(),
};
assert_eq!(expected, directory.render(&TestColours, SizeFormat::BinaryBytes, &NumericLocale::english()))
assert_eq!(
expected,
directory.render(
&TestColours,
SizeFormat::BinaryBytes,
&NumericLocale::english()
)
)
}
#[test]
fn file_bytes() {
let directory = f::Size::Some(1_048_576);
let expected = TextCell {
width: DisplayWidth::from(9),
contents: vec![
Fixed(66).paint("1,048,576"),
].into(),
contents: vec![Fixed(66).paint("1,048,576")].into(),
};
assert_eq!(expected, directory.render(&TestColours, SizeFormat::JustBytes, &NumericLocale::english()))
assert_eq!(
expected,
directory.render(
&TestColours,
SizeFormat::JustBytes,
&NumericLocale::english()
)
)
}
#[test]
fn device_ids() {
let directory = f::Size::DeviceIDs(f::DeviceIDs { major: 10, minor: 80 });
let directory = f::Size::DeviceIDs(f::DeviceIDs {
major: 10,
minor: 80,
});
let expected = TextCell {
width: DisplayWidth::from(5),
contents: vec![
Blue.on(Red).paint("10"),
Green.italic().paint(","),
Cyan.on(Yellow).paint("80"),
].into(),
]
.into(),
};
assert_eq!(expected, directory.render(&TestColours, SizeFormat::JustBytes, &NumericLocale::english()))
assert_eq!(
expected,
directory.render(
&TestColours,
SizeFormat::JustBytes,
&NumericLocale::english()
)
)
}
}

View File

@ -4,7 +4,6 @@ use crate::output::time::TimeFormat;
use ansiterm::Style;
use chrono::prelude::*;
pub trait Render {
fn render(self, style: Style, time_offset: FixedOffset, time_format: TimeFormat) -> TextCell;
}
@ -12,7 +11,10 @@ pub trait Render {
impl Render for Option<NaiveDateTime> {
fn render(self, style: Style, time_offset: FixedOffset, time_format: TimeFormat) -> TextCell {
let datestamp = if let Some(time) = self {
time_format.format(&DateTime::<FixedOffset>::from_naive_utc_and_offset(time, time_offset))
time_format.format(&DateTime::<FixedOffset>::from_naive_utc_and_offset(
time,
time_offset,
))
} else {
String::from("-")
};

View File

@ -11,30 +11,33 @@ pub trait Render {
impl Render for Option<f::User> {
fn render<C: Colours, U: Users>(self, colours: &C, users: &U, format: UserFormat) -> TextCell {
#[rustfmt::skip]
let uid = match self {
Some(u) => u.0,
None => return TextCell::blank(colours.no_user()),
};
#[rustfmt::skip]
let user_name = match (format, users.get_user_by_uid(uid)) {
(_, None) => uid.to_string(),
(UserFormat::Numeric, _) => uid.to_string(),
(UserFormat::Name, Some(user)) => user.name().to_string_lossy().into(),
};
let style = if users.get_current_uid() == uid { colours.you() }
else { colours.someone_else() };
let style = if users.get_current_uid() == uid {
colours.you()
} else {
colours.someone_else()
};
TextCell::paint(style, user_name)
}
}
pub trait Colours {
fn you(&self) -> Style;
fn someone_else(&self) -> Style;
fn no_user(&self) -> Style;
}
#[cfg(test)]
#[allow(unused_results)]
pub mod test {
@ -43,21 +46,20 @@ pub mod test {
use crate::output::cell::TextCell;
use crate::output::table::UserFormat;
use uzers::User;
use uzers::mock::MockUsers;
use ansiterm::Colour::*;
use ansiterm::Style;
use uzers::mock::MockUsers;
use uzers::User;
struct TestColours;
#[rustfmt::skip]
impl Colours for TestColours {
fn you(&self) -> Style { Red.bold() }
fn someone_else(&self) -> Style { Blue.underline() }
fn no_user(&self) -> Style { Black.italic() }
}
#[test]
fn named() {
let mut users = MockUsers::with_current_uid(1000);
@ -65,9 +67,11 @@ pub mod test {
let user = Some(f::User(1000));
let expected = TextCell::paint_str(Red.bold(), "enoch");
#[rustfmt::skip]
assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
let expected = TextCell::paint_str(Red.bold(), "1000");
#[rustfmt::skip]
assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Numeric));
}
@ -77,7 +81,9 @@ pub mod test {
let user = Some(f::User(1000));
let expected = TextCell::paint_str(Red.bold(), "1000");
#[rustfmt::skip]
assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
#[rustfmt::skip]
assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Numeric));
}
@ -88,20 +94,37 @@ pub mod test {
let user = Some(f::User(1000));
let expected = TextCell::paint_str(Blue.underline(), "enoch");
assert_eq!(expected, user.render(&TestColours, &users, UserFormat::Name));
assert_eq!(
expected,
user.render(&TestColours, &users, UserFormat::Name)
);
}
#[test]
fn different_unnamed() {
let user = Some(f::User(1000));
let expected = TextCell::paint_str(Blue.underline(), "1000");
assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
assert_eq!(
expected,
user.render(
&TestColours,
&MockUsers::with_current_uid(0),
UserFormat::Numeric
)
);
}
#[test]
fn overflow() {
let user = Some(f::User(2_147_483_648));
let expected = TextCell::paint_str(Blue.underline(), "2147483648");
assert_eq!(expected, user.render(&TestColours, &MockUsers::with_current_uid(0), UserFormat::Numeric));
assert_eq!(
expected,
user.render(
&TestColours,
&MockUsers::with_current_uid(0),
UserFormat::Numeric
)
);
}
}

View File

@ -10,22 +10,15 @@ use log::*;
#[cfg(unix)]
use uzers::UsersCache;
use crate::fs::{File, fields as f};
use crate::fs::feature::git::GitCache;
use crate::fs::{fields as f, File};
use crate::output::cell::TextCell;
use crate::output::render::{PermissionsPlusRender, TimeRender};
#[cfg(unix)]
use crate::output::render::{
GroupRender,
OctalPermissionsRender,
UserRender
};
use crate::output::render::{GroupRender, OctalPermissionsRender, UserRender};
use crate::output::render::{PermissionsPlusRender, TimeRender};
use crate::output::time::TimeFormat;
use crate::theme::Theme;
/// Options for displaying a table.
#[derive(PartialEq, Eq, Debug)]
pub struct Options {
@ -39,7 +32,6 @@ pub struct Options {
#[allow(clippy::struct_excessive_bools)]
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct Columns {
/// At least one of these timestamps will be shown.
pub time_types: TimeTypes,
@ -139,7 +131,6 @@ impl Columns {
}
}
/// A table contains these.
#[derive(Debug, Copy, Clone)]
pub enum Column {
@ -173,24 +164,25 @@ pub enum Alignment {
}
impl Column {
/// Get the alignment this column should use.
#[cfg(unix)]
pub fn alignment(self) -> Alignment {
#[allow(clippy::wildcard_in_or_patterns)]
#[rustfmt::skip]
match self {
Self::FileSize |
Self::HardLinks |
Self::Inode |
Self::Blocksize |
Self::GitStatus => Alignment::Right,
Self::Timestamp(_) |
Self::Timestamp(_) |
_ => Alignment::Left,
}
}
#[cfg(windows)]
pub fn alignment(self) -> Alignment {
#[rustfmt::skip]
match self {
Self::FileSize |
Self::GitStatus => Alignment::Right,
@ -201,6 +193,7 @@ impl Column {
/// Get the text that should be printed at the top, when the user elects
/// to have a header row printed.
pub fn header(self) -> &'static str {
#[rustfmt::skip]
match self {
#[cfg(unix)]
Self::Permissions => "Permissions",
@ -228,12 +221,10 @@ impl Column {
}
}
/// Formatting options for file sizes.
#[allow(clippy::enum_variant_names)]
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum SizeFormat {
/// Format the file size using **decimal** prefixes, such as “kilo”,
/// “mega”, or “giga”.
DecimalBytes,
@ -261,12 +252,10 @@ impl Default for SizeFormat {
}
}
/// The types of a files time fields. These three fields are standard
/// across most (all?) operating systems.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum TimeType {
/// The files modified time (`st_mtime`).
Modified,
@ -281,9 +270,9 @@ pub enum TimeType {
}
impl TimeType {
/// Returns the text to use for a columns heading in the columns output.
pub fn header(self) -> &'static str {
#[rustfmt::skip]
match self {
Self::Modified => "Date Modified",
Self::Changed => "Date Changed",
@ -293,13 +282,13 @@ impl TimeType {
}
}
/// Fields for which of a files time fields should be displayed in the
/// columns output.
///
/// There should always be at least one of these — theres no way to disable
/// the time columns entirely (yet).
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
#[rustfmt::skip]
#[allow(clippy::struct_excessive_bools)]
pub struct TimeTypes {
pub modified: bool,
@ -309,10 +298,10 @@ pub struct TimeTypes {
}
impl Default for TimeTypes {
/// By default, display just the modified time. This is the most
/// common option, which is why it has this shorthand.
fn default() -> Self {
#[rustfmt::skip]
Self {
modified: true,
changed: false,
@ -322,13 +311,11 @@ impl Default for TimeTypes {
}
}
/// The **environment** struct contains any data that could change between
/// running instances of exa, depending on the users computers configuration.
///
/// Any environment field should be able to be mocked up for test runs.
pub struct Environment {
/// The computers current time offset, determined from time zone.
time_offset: FixedOffset,
@ -349,13 +336,18 @@ impl Environment {
fn load_all() -> Self {
let time_offset = *Local::now().offset();
let numeric = locale::Numeric::load_user_locale()
.unwrap_or_else(|_| locale::Numeric::english());
let numeric =
locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english());
#[cfg(unix)]
let users = Mutex::new(UsersCache::new());
Self { time_offset, numeric, #[cfg(unix)] users }
Self {
time_offset,
numeric,
#[cfg(unix)]
users,
}
}
}
@ -363,7 +355,6 @@ lazy_static! {
static ref ENVIRONMENT: Environment = Environment::load_all();
}
pub struct Table<'a> {
columns: Vec<Column>,
theme: &'a Theme,
@ -405,17 +396,21 @@ impl<'a> Table<'a> {
}
pub fn header_row(&self) -> Row {
let cells = self.columns.iter()
.map(|c| TextCell::paint_str(self.theme.ui.header, c.header()))
.collect();
let cells = self
.columns
.iter()
.map(|c| TextCell::paint_str(self.theme.ui.header, c.header()))
.collect();
Row { cells }
}
pub fn row_for_file(&self, file: &File<'_>, xattrs: bool) -> Row {
let cells = self.columns.iter()
.map(|c| self.display(file, *c, xattrs))
.collect();
let cells = self
.columns
.iter()
.map(|c| self.display(file, *c, xattrs))
.collect();
Row { cells }
}
@ -429,7 +424,7 @@ impl<'a> Table<'a> {
file.permissions().map(|p| f::PermissionsPlus {
file_type: file.type_char(),
permissions: p,
xattrs
xattrs,
})
}
@ -446,12 +441,12 @@ impl<'a> Table<'a> {
#[cfg(unix)]
fn octal_permissions(&self, file: &File<'_>) -> Option<f::OctalPermissions> {
file.permissions().map(|p| f::OctalPermissions {
permissions: p,
})
file.permissions()
.map(|p| f::OctalPermissions { permissions: p })
}
fn display(&self, file: &File<'_>, column: Column, xattrs: bool) -> TextCell {
#[rustfmt::skip]
match column {
Column::Permissions => {
self.permissions_plus(file, xattrs).render(self.theme)
@ -469,15 +464,18 @@ impl<'a> Table<'a> {
}
#[cfg(unix)]
Column::Blocksize => {
file.blocksize().render(self.theme, self.size_format, &self.env.numeric)
file.blocksize()
.render(self.theme, self.size_format, &self.env.numeric)
}
#[cfg(unix)]
Column::User => {
file.user().render(self.theme, &*self.env.lock_users(), self.user_format)
file.user()
.render(self.theme, &*self.env.lock_users(), self.user_format)
}
#[cfg(unix)]
Column::Group => {
file.group().render(self.theme, &*self.env.lock_users(), self.user_format)
file.group()
.render(self.theme, &*self.env.lock_users(), self.user_format)
}
#[cfg(unix)]
Column::SecurityContext => {
@ -517,10 +515,10 @@ impl<'a> Table<'a> {
.unwrap_or_default()
}
fn subdir_git_repo(&self, file: &File<'_>, status : bool) -> f::SubdirGitRepo {
fn subdir_git_repo(&self, file: &File<'_>, status: bool) -> f::SubdirGitRepo {
debug!("Getting subdir repo status for path {:?}", file.path);
if file.is_directory(){
if file.is_directory() {
return f::SubdirGitRepo::from_path(&file.path, status);
}
f::SubdirGitRepo::default()
@ -529,9 +527,7 @@ impl<'a> Table<'a> {
pub fn render(&self, row: Row) -> TextCell {
let mut cell = TextCell::default();
let iter = row.cells.into_iter()
.zip(self.widths.iter())
.enumerate();
let iter = row.cells.into_iter().zip(self.widths.iter()).enumerate();
for (n, (this_cell, width)) in iter {
let padding = width - *this_cell.width;
@ -554,7 +550,6 @@ impl<'a> Table<'a> {
}
}
pub struct TableWidths(Vec<usize>);
impl Deref for TableWidths {

View File

@ -1,12 +1,11 @@
//! Timestamp formatting.
use core::cmp::max;
use std::time::Duration;
use chrono::prelude::*;
use core::cmp::max;
use lazy_static::lazy_static;
use std::time::Duration;
use unicode_width::UnicodeWidthStr;
/// Every timestamp in exa needs to be rendered by a **time format**.
/// Formatting times is tricky, because how a timestamp is rendered can
/// depend on one or more of the following:
@ -25,7 +24,6 @@ use unicode_width::UnicodeWidthStr;
/// format string in an environment variable or something. Just these four.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum TimeFormat {
/// The **default format** uses the users locale to print month names,
/// and specifies the timestamp down to the minute for recent times, and
/// day for older times.
@ -51,6 +49,7 @@ pub enum TimeFormat {
impl TimeFormat {
pub fn format(self, time: &DateTime<FixedOffset>) -> String {
#[rustfmt::skip]
match self {
Self::DefaultFormat => default(time),
Self::ISOFormat => iso(time),
@ -99,21 +98,19 @@ fn long(time: &DateTime<FixedOffset>) -> String {
fn relative(time: &DateTime<FixedOffset>) -> String {
timeago::Formatter::new()
.ago("")
.convert(
Duration::from_secs(
max(0, Local::now().timestamp() - time.timestamp())
// this .unwrap is safe since the call above can never result in a
.convert(Duration::from_secs(
max(0, Local::now().timestamp() - time.timestamp())
// this .unwrap is safe since the call above can never result in a
// value < 0
.try_into().unwrap()
)
)
.try_into()
.unwrap(),
))
}
fn full(time: &DateTime<FixedOffset>) -> String {
time.format("%Y-%m-%d %H:%M:%S.%f %z").to_string()
}
lazy_static! {
static ref CURRENT_YEAR: i32 = Local::now().year();
@ -147,25 +144,29 @@ mod test {
#[test]
fn short_month_width_hindi() {
let max_month_width = 4;
assert_eq!(true, [
"\u{091C}\u{0928}\u{0970}", // जन॰
"\u{092B}\u{093C}\u{0930}\u{0970}", // फ़र॰
"\u{092E}\u{093E}\u{0930}\u{094D}\u{091A}", // मार्च
"\u{0905}\u{092A}\u{094D}\u{0930}\u{0948}\u{0932}", // अप्रैल
"\u{092E}\u{0908}", // मई
"\u{091C}\u{0942}\u{0928}", // जून
"\u{091C}\u{0941}\u{0932}\u{0970}", // जुल॰
"\u{0905}\u{0917}\u{0970}", // अग॰
"\u{0938}\u{093F}\u{0924}\u{0970}", // सित॰
"\u{0905}\u{0915}\u{094D}\u{0924}\u{0942}\u{0970}", // अक्तू॰
"\u{0928}\u{0935}\u{0970}", // नव॰
"\u{0926}\u{093F}\u{0938}\u{0970}", // दिस॰
].iter()
assert_eq!(
true,
[
"\u{091C}\u{0928}\u{0970}", // जन॰
"\u{092B}\u{093C}\u{0930}\u{0970}", // फ़र॰
"\u{092E}\u{093E}\u{0930}\u{094D}\u{091A}", // मार्च
"\u{0905}\u{092A}\u{094D}\u{0930}\u{0948}\u{0932}", // अप्रैल
"\u{092E}\u{0908}", // मई
"\u{091C}\u{0942}\u{0928}", // जून
"\u{091C}\u{0941}\u{0932}\u{0970}", // जुल॰
"\u{0905}\u{0917}\u{0970}", // अग॰
"\u{0938}\u{093F}\u{0924}\u{0970}", // सित॰
"\u{0905}\u{0915}\u{094D}\u{0924}\u{0942}\u{0970}", // अक्तू॰
"\u{0928}\u{0935}\u{0970}", // नव॰
"\u{0926}\u{093F}\u{0938}\u{0970}", // दिस॰
]
.iter()
.map(|month| format!(
"{:<width$}",
month,
width = short_month_padding(max_month_width, month)
)).all(|string| UnicodeWidthStr::width(string.as_str()) == max_month_width)
))
.all(|string| UnicodeWidthStr::width(string.as_str()) == max_month_width)
);
}
}

View File

@ -38,10 +38,8 @@
//! successfully `stat`ted, we dont know how many files are going to exist in
//! each directory)
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum TreePart {
/// Rightmost column, *not* the last in the directory.
Edge,
@ -56,10 +54,10 @@ pub enum TreePart {
}
impl TreePart {
/// Turn this tree part into ASCII-licious box drawing characters!
/// (Warning: not actually ASCII)
pub fn ascii_art(self) -> &'static str {
#[rustfmt::skip]
match self {
Self::Edge => "├──",
Self::Line => "",
@ -69,11 +67,9 @@ impl TreePart {
}
}
/// A **tree trunk** builds up arrays of tree parts over multiple depths.
#[derive(Debug, Default)]
pub struct TreeTrunk {
/// A stack tracks which tree characters should be printed. Its
/// necessary to maintain information about the previously-printed
/// lines, as the output will change based on any previous entries.
@ -85,7 +81,6 @@ pub struct TreeTrunk {
#[derive(Debug, Copy, Clone)]
pub struct TreeParams {
/// How many directories deep into the tree structure this is. Directories
/// on top have depth 0.
depth: TreeDepth,
@ -98,7 +93,6 @@ pub struct TreeParams {
pub struct TreeDepth(pub usize);
impl TreeTrunk {
/// Calculates the tree parts for an entry at the given depth and
/// last-ness. The depth is used to determine where in the stack the tree
/// part should be inserted, and the last-ness is used to determine which
@ -107,19 +101,24 @@ impl TreeTrunk {
/// This takes a `&mut self` because the results of each file are stored
/// and used in future rows.
pub fn new_row(&mut self, params: TreeParams) -> &[TreePart] {
// If this isnt our first iteration, then update the tree parts thus
// far to account for there being another row after it.
if let Some(last) = self.last_params {
self.stack[last.depth.0] = if last.last { TreePart::Blank }
else { TreePart::Line };
self.stack[last.depth.0] = if last.last {
TreePart::Blank
} else {
TreePart::Line
};
}
// Make sure the stack has enough space, then add or modify another
// part into it.
self.stack.resize(params.depth.0 + 1, TreePart::Edge);
self.stack[params.depth.0] = if params.last { TreePart::Corner }
else { TreePart::Edge };
self.stack[params.depth.0] = if params.last {
TreePart::Corner
} else {
TreePart::Edge
};
self.last_params = Some(params);
@ -162,20 +161,24 @@ impl TreeDepth {
/// Creates an iterator that, as well as yielding each value, yields a
/// `TreeParams` with the current depth and last flag filled in.
pub fn iterate_over<I, T>(self, inner: I) -> Iter<I>
where I: ExactSizeIterator + Iterator<Item = T>
where
I: ExactSizeIterator + Iterator<Item = T>,
{
Iter { current_depth: self, inner }
Iter {
current_depth: self,
inner,
}
}
}
pub struct Iter<I> {
current_depth: TreeDepth,
inner: I,
}
impl<I, T> Iterator for Iter<I>
where I: ExactSizeIterator + Iterator<Item = T>
where
I: ExactSizeIterator + Iterator<Item = T>,
{
type Item = (TreeParams, T);
@ -188,7 +191,6 @@ where I: ExactSizeIterator + Iterator<Item = T>
}
}
#[cfg(test)]
mod trunk_test {
use super::*;
@ -197,12 +199,14 @@ mod trunk_test {
TreeParams::new(TreeDepth(depth), last)
}
#[rustfmt::skip]
#[test]
fn empty_at_first() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, true)), &[ ]);
}
#[rustfmt::skip]
#[test]
fn one_child() {
let mut tt = TreeTrunk::default();
@ -210,6 +214,7 @@ mod trunk_test {
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
#[rustfmt::skip]
#[test]
fn two_children() {
let mut tt = TreeTrunk::default();
@ -218,6 +223,7 @@ mod trunk_test {
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
#[rustfmt::skip]
#[test]
fn two_times_two_children() {
let mut tt = TreeTrunk::default();
@ -230,6 +236,7 @@ mod trunk_test {
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
#[rustfmt::skip]
#[test]
fn two_times_two_nested_children() {
let mut tt = TreeTrunk::default();
@ -245,14 +252,13 @@ mod trunk_test {
}
}
#[cfg(test)]
mod iter_test {
use super::*;
#[test]
fn test_iteration() {
let foos = &[ "first", "middle", "last" ];
let foos = &["first", "middle", "last"];
let mut iter = TreeDepth::root().iterate_over(foos.iter());
let next = iter.next().unwrap();

View File

@ -1,15 +1,15 @@
use ansiterm::Style;
use ansiterm::Colour::*;
use ansiterm::Style;
use crate::theme::ColourScale;
use crate::theme::ui_styles::*;
use crate::theme::ColourScale;
impl UiStyles {
pub fn default_theme(scale: ColourScale) -> Self {
Self {
colourful: true,
#[rustfmt::skip]
filekinds: FileKinds {
normal: Style::default(),
directory: Blue.bold(),
@ -23,6 +23,7 @@ impl UiStyles {
mount_point: Blue.bold().underline(),
},
#[rustfmt::skip]
perms: Permissions {
user_read: Yellow.bold(),
user_write: Red.bold(),
@ -45,6 +46,7 @@ impl UiStyles {
size: Size::colourful(scale),
#[rustfmt::skip]
users: Users {
user_you: Yellow.bold(),
user_someone_else: Style::default(),
@ -52,11 +54,13 @@ impl UiStyles {
group_not_yours: Style::default(),
},
#[rustfmt::skip]
links: Links {
normal: Red.bold(),
multi_link_file: Red.on(Yellow),
},
#[rustfmt::skip]
git: Git {
new: Green.normal(),
modified: Blue.normal(),
@ -75,7 +79,8 @@ impl UiStyles {
},
security_context: SecurityContext {
none: Style::default(),
none: Style::default(),
#[rustfmt::skip]
selinux: SELinuxContext {
colon: Style::default().dimmed(),
user: Blue.normal(),
@ -85,47 +90,47 @@ impl UiStyles {
},
},
#[rustfmt::skip]
file_type: FileType {
image: Purple.normal(),
video: Purple.bold(),
music: Cyan.normal(),
lossless: Cyan.bold(),
crypto: Green.bold(),
document: Green.normal(),
image: Purple.normal(),
video: Purple.bold(),
music: Cyan.normal(),
lossless: Cyan.bold(),
crypto: Green.bold(),
document: Green.normal(),
compressed: Red.normal(),
temp: White.normal(),
compiled: Yellow.normal(),
build: Yellow.bold().underline()
temp: White.normal(),
compiled: Yellow.normal(),
build: Yellow.bold().underline(),
},
punctuation: DarkGray.bold(),
date: Blue.normal(),
inode: Purple.normal(),
blocks: Cyan.normal(),
octal: Purple.normal(),
header: Style::default().underline(),
punctuation: DarkGray.bold(),
date: Blue.normal(),
inode: Purple.normal(),
blocks: Cyan.normal(),
octal: Purple.normal(),
header: Style::default().underline(),
symlink_path: Cyan.normal(),
control_char: Red.normal(),
broken_symlink: Red.normal(),
broken_path_overlay: Style::default().underline(),
symlink_path: Cyan.normal(),
control_char: Red.normal(),
broken_symlink: Red.normal(),
broken_path_overlay: Style::default().underline(),
}
}
}
impl Size {
pub fn colourful(scale: ColourScale) -> Self {
match scale {
ColourScale::Gradient => Self::colourful_gradient(),
ColourScale::Fixed => Self::colourful_fixed(),
ColourScale::Gradient => Self::colourful_gradient(),
ColourScale::Fixed => Self::colourful_fixed(),
}
}
fn colourful_fixed() -> Self {
Self {
major: Green.bold(),
minor: Green.normal(),
major: Green.bold(),
minor: Green.normal(),
number_byte: Green.bold(),
number_kilo: Green.bold(),
@ -143,8 +148,8 @@ impl Size {
fn colourful_gradient() -> Self {
Self {
major: Green.bold(),
minor: Green.normal(),
major: Green.bold(),
minor: Green.normal(),
number_byte: Green.normal(),
number_kilo: Green.bold(),

View File

@ -1,9 +1,8 @@
use std::iter::Peekable;
use std::ops::FnMut;
use ansiterm::{Colour, Style};
use ansiterm::Colour::*;
use ansiterm::{Colour, Style};
// Parsing the LS_COLORS environment variable into a map of names to Style values.
//
@ -26,23 +25,25 @@ pub struct LSColors<'var>(pub &'var str);
impl<'var> LSColors<'var> {
pub fn each_pair<C>(&mut self, mut callback: C)
where C: FnMut(Pair<'var>)
where
C: FnMut(Pair<'var>),
{
for next in self.0.split(':') {
let bits = next.split('=')
.take(3)
.collect::<Vec<_>>();
let bits = next.split('=').take(3).collect::<Vec<_>>();
if bits.len() == 2 && ! bits[0].is_empty() && ! bits[1].is_empty() {
callback(Pair { key: bits[0], value: bits[1] });
if bits.len() == 2 && !bits[0].is_empty() && !bits[1].is_empty() {
callback(Pair {
key: bits[0],
value: bits[1],
});
}
}
}
}
fn parse_into_high_colour<'a, I>(iter: &mut Peekable<I>) -> Option<Colour>
where I: Iterator<Item = &'a str>
where
I: Iterator<Item = &'a str>,
{
match iter.peek() {
Some(&"5") => {
@ -69,22 +70,22 @@ where I: Iterator<Item = &'a str>
}
}*/
if let (Some(r), Some(g), Some(b)) = (hexes.parse().ok(),
iter.next().and_then(|s| s.parse().ok()),
iter.next().and_then(|s| s.parse().ok()))
{
if let (Some(r), Some(g), Some(b)) = (
hexes.parse().ok(),
iter.next().and_then(|s| s.parse().ok()),
iter.next().and_then(|s| s.parse().ok()),
) {
return Some(RGB(r, g, b));
}
}
}
_ => {},
_ => {}
}
None
}
pub struct Pair<'var> {
pub key: &'var str,
pub value: &'var str,
@ -97,7 +98,6 @@ impl<'var> Pair<'var> {
while let Some(num) = iter.next() {
match num.trim_start_matches('0') {
// Bold and italic
"1" => style = style.bold(),
"2" => style = style.dimmed(),
@ -127,7 +127,11 @@ impl<'var> Pair<'var> {
"95" => style = style.fg(BrightPurple),
"96" => style = style.fg(BrightCyan),
"97" => style = style.fg(BrightGray),
"38" => if let Some(c) = parse_into_high_colour(&mut iter) { style = style.fg(c) },
"38" => {
if let Some(c) = parse_into_high_colour(&mut iter) {
style = style.fg(c)
}
}
// Background colours
"40" => style = style.on(Black),
@ -147,8 +151,12 @@ impl<'var> Pair<'var> {
"105" => style = style.on(BrightPurple),
"106" => style = style.on(BrightCyan),
"107" => style = style.on(BrightGray),
"48" => if let Some(c) = parse_into_high_colour(&mut iter) { style = style.on(c) },
_ => {/* ignore the error and do nothing */},
"48" => {
if let Some(c) = parse_into_high_colour(&mut iter) {
style = style.on(c)
}
}
_ => { /* ignore the error and do nothing */ }
}
}
@ -156,7 +164,6 @@ impl<'var> Pair<'var> {
}
}
#[cfg(test)]
mod ansi_test {
use super::*;
@ -166,7 +173,14 @@ mod ansi_test {
($name:ident: $input:expr => $result:expr) => {
#[test]
fn $name() {
assert_eq!(Pair { key: "", value: $input }.to_style(), $result);
assert_eq!(
Pair {
key: "",
value: $input
}
.to_style(),
$result
);
}
};
}
@ -207,7 +221,6 @@ mod ansi_test {
test!(toohi: "48;5;999" => Style::default());
}
#[cfg(test)]
mod test {
use super::*;
@ -217,7 +230,7 @@ mod test {
#[test]
fn $name() {
let mut lscs = Vec::new();
LSColors($input).each_pair(|p| lscs.push( (p.key.clone(), p.to_style()) ));
LSColors($input).each_pair(|p| lscs.push((p.key.clone(), p.to_style())));
assert_eq!(lscs, $result.to_vec());
}
};

View File

@ -13,10 +13,8 @@ pub use self::lsc::LSColors;
mod default_theme;
#[derive(PartialEq, Eq, Debug)]
pub struct Options {
pub use_colours: UseColours,
pub colour_scale: ColourScale,
@ -33,7 +31,6 @@ pub struct Options {
/// this check and only displays colours when they can be truly appreciated.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum UseColours {
/// Display them even when output isnt going to a terminal.
Always,
@ -56,17 +53,17 @@ pub struct Definitions {
pub exa: Option<String>,
}
pub struct Theme {
pub ui: UiStyles,
pub exts: Box<dyn FileStyle>,
}
impl Options {
#[allow(trivial_casts)] // the `as Box<_>` stuff below warns about this for some reason
#[allow(trivial_casts)] // the `as Box<_>` stuff below warns about this for some reason
pub fn to_theme(&self, isatty: bool) -> Theme {
if self.use_colours == UseColours::Never || (self.use_colours == UseColours::Automatic && ! isatty) {
if self.use_colours == UseColours::Never
|| (self.use_colours == UseColours::Automatic && !isatty)
{
let ui = UiStyles::plain();
let exts = Box::new(NoFileStyle);
return Theme { ui, exts };
@ -77,6 +74,7 @@ impl Options {
let (exts, use_default_filetypes) = self.definitions.parse_color_vars(&mut ui);
// Use between 0 and 2 file name highlighters
#[rustfmt::skip]
let exts = match (exts.is_non_empty(), use_default_filetypes) {
(false, false) => Box::new(NoFileStyle) as Box<_>,
(false, true) => Box::new(FileTypes) as Box<_>,
@ -89,7 +87,6 @@ impl Options {
}
impl Definitions {
/// Parse the environment variables into `LS_COLORS` pairs, putting file glob
/// colours into the `ExtensionMappings` that gets returned, and using the
/// two-character UI codes to modify the mutable `Colours`.
@ -103,7 +100,7 @@ impl Definitions {
if let Some(lsc) = &self.ls {
LSColors(lsc).each_pair(|pair| {
if ! colours.set_ls(&pair) {
if !colours.set_ls(&pair) {
match glob::Pattern::new(pair.key) {
Ok(pat) => {
exts.add(pat, pair.to_style());
@ -125,7 +122,7 @@ impl Definitions {
}
LSColors(exa).each_pair(|pair| {
if ! colours.set_ls(&pair) && ! colours.set_exa(&pair) {
if !colours.set_ls(&pair) && !colours.set_exa(&pair) {
match glob::Pattern::new(pair.key) {
Ok(pat) => {
exts.add(pat, pair.to_style());
@ -142,7 +139,6 @@ impl Definitions {
}
}
/// Determine the style to paint the text for the filename part of the output.
pub trait FileStyle: Sync {
/// Return the style to paint the filename text for `file` from the given
@ -164,16 +160,17 @@ impl FileStyle for NoFileStyle {
// file type associations, while falling back to the default set if not set
// explicitly.
impl<A, B> FileStyle for (A, B)
where A: FileStyle,
B: FileStyle,
where
A: FileStyle,
B: FileStyle,
{
fn get_style(&self, file: &File<'_>, theme: &Theme) -> Option<Style> {
self.0.get_style(file, theme)
self.0
.get_style(file, theme)
.or_else(|| self.1.get_style(file, theme))
}
}
#[derive(PartialEq, Debug, Default)]
struct ExtensionMappings {
mappings: Vec<(glob::Pattern, Style)>,
@ -181,7 +178,7 @@ struct ExtensionMappings {
impl ExtensionMappings {
fn is_non_empty(&self) -> bool {
! self.mappings.is_empty()
!self.mappings.is_empty()
}
fn add(&mut self, pattern: glob::Pattern, style: Style) {
@ -194,9 +191,11 @@ impl ExtensionMappings {
impl FileStyle for ExtensionMappings {
fn get_style(&self, file: &File<'_>, _theme: &Theme) -> Option<Style> {
self.mappings.iter().rev()
self.mappings
.iter()
.rev()
.find(|t| t.0.matches(&file.name))
.map (|t| t.1)
.map(|t| t.1)
}
}
@ -205,6 +204,7 @@ struct FileTypes;
impl FileStyle for FileTypes {
fn get_style(&self, file: &File<'_>, theme: &Theme) -> Option<Style> {
#[rustfmt::skip]
match FileType::get_file_type(file) {
Some(FileType::Image) => Some(theme.ui.file_type.image),
Some(FileType::Video) => Some(theme.ui.file_type.video),
@ -226,6 +226,7 @@ impl render::BlocksColours for Theme {
fn blocksize(&self, prefix: Option<number_prefix::Prefix>) -> Style {
use number_prefix::Prefix::*;
#[rustfmt::skip]
match prefix {
Some(Kilo | Kibi) => self.ui.size.number_kilo,
Some(Mega | Mebi) => self.ui.size.number_mega,
@ -238,6 +239,7 @@ impl render::BlocksColours for Theme {
fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
use number_prefix::Prefix::*;
#[rustfmt::skip]
match prefix {
Some(Kilo | Kibi) => self.ui.size.unit_kilo,
Some(Mega | Mebi) => self.ui.size.unit_mega,
@ -247,9 +249,12 @@ impl render::BlocksColours for Theme {
}
}
fn no_blocksize(&self) -> Style { self.ui.punctuation }
fn no_blocksize(&self) -> Style {
self.ui.punctuation
}
}
#[rustfmt::skip]
impl render::FiletypeColours for Theme {
fn normal(&self) -> Style { self.ui.filekinds.normal }
fn directory(&self) -> Style { self.ui.filekinds.directory }
@ -261,6 +266,7 @@ impl render::FiletypeColours for Theme {
fn special(&self) -> Style { self.ui.filekinds.special }
}
#[rustfmt::skip]
impl render::GitColours for Theme {
fn not_modified(&self) -> Style { self.ui.punctuation }
#[allow(clippy::new_ret_no_self)]
@ -273,14 +279,16 @@ impl render::GitColours for Theme {
fn conflicted(&self) -> Style { self.ui.git.conflicted }
}
#[rustfmt::skip]
impl render::GitRepoColours for Theme {
fn branch_main(&self) -> Style { self.ui.git_repo.branch_main }
fn branch_main(&self) -> Style { self.ui.git_repo.branch_main }
fn branch_other(&self) -> Style { self.ui.git_repo.branch_other }
fn no_repo(&self) -> Style { self.ui.punctuation }
fn git_clean(&self) -> Style { self.ui.git_repo.git_clean }
fn git_dirty(&self) -> Style { self.ui.git_repo.git_dirty }
fn no_repo(&self) -> Style { self.ui.punctuation }
fn git_clean(&self) -> Style { self.ui.git_repo.git_clean }
fn git_dirty(&self) -> Style { self.ui.git_repo.git_dirty }
}
#[rustfmt::skip]
#[cfg(unix)]
impl render::GroupColours for Theme {
fn yours(&self) -> Style { self.ui.users.group_yours }
@ -288,11 +296,13 @@ impl render::GroupColours for Theme {
fn no_group(&self) -> Style { self.ui.punctuation }
}
#[rustfmt::skip]
impl render::LinksColours for Theme {
fn normal(&self) -> Style { self.ui.links.normal }
fn multi_link_file(&self) -> Style { self.ui.links.multi_link_file }
}
#[rustfmt::skip]
impl render::PermissionsColours for Theme {
fn dash(&self) -> Style { self.ui.punctuation }
fn user_read(&self) -> Style { self.ui.perms.user_read }
@ -314,6 +324,7 @@ impl render::SizeColours for Theme {
fn size(&self, prefix: Option<number_prefix::Prefix>) -> Style {
use number_prefix::Prefix::*;
#[rustfmt::skip]
match prefix {
Some(Kilo | Kibi) => self.ui.size.number_kilo,
Some(Mega | Mebi) => self.ui.size.number_mega,
@ -326,6 +337,7 @@ impl render::SizeColours for Theme {
fn unit(&self, prefix: Option<number_prefix::Prefix>) -> Style {
use number_prefix::Prefix::*;
#[rustfmt::skip]
match prefix {
Some(Kilo | Kibi) => self.ui.size.unit_kilo,
Some(Mega | Mebi) => self.ui.size.unit_mega,
@ -335,12 +347,17 @@ impl render::SizeColours for Theme {
}
}
#[rustfmt::skip]
fn no_size(&self) -> Style { self.ui.punctuation }
#[rustfmt::skip]
fn major(&self) -> Style { self.ui.size.major }
#[rustfmt::skip]
fn comma(&self) -> Style { self.ui.punctuation }
#[rustfmt::skip]
fn minor(&self) -> Style { self.ui.size.minor }
}
#[rustfmt::skip]
#[cfg(unix)]
impl render::UserColours for Theme {
fn you(&self) -> Style { self.ui.users.user_you }
@ -348,6 +365,7 @@ impl render::UserColours for Theme {
fn no_user(&self) -> Style { self.ui.punctuation }
}
#[rustfmt::skip]
impl FileNameColours for Theme {
fn symlink_path(&self) -> Style { self.ui.symlink_path }
fn normal_arrow(&self) -> Style { self.ui.punctuation }
@ -359,10 +377,13 @@ impl FileNameColours for Theme {
fn mount_point(&self) -> Style { self.ui.filekinds.mount_point }
fn colour_file(&self, file: &File<'_>) -> Style {
self.exts.get_style(file, self).unwrap_or(self.ui.filekinds.normal)
self.exts
.get_style(file, self)
.unwrap_or(self.ui.filekinds.normal)
}
}
#[rustfmt::skip]
impl render::SecurityCtxColours for Theme {
fn none(&self) -> Style { self.ui.security_context.none }
fn selinux_colon(&self) -> Style { self.ui.security_context.selinux.colon }
@ -372,7 +393,6 @@ impl render::SecurityCtxColours for Theme {
fn selinux_range(&self) -> Style { self.ui.security_context.selinux.range }
}
/// Some of the styles are **overlays**: although they have the same attribute
/// set as regular styles (foreground and background colours, bold, underline,
/// etc), theyre intended to be used to *amend* existing styles.
@ -385,6 +405,7 @@ impl render::SecurityCtxColours for Theme {
/// character”, there are styles for “link path”, “control character”, and
/// “broken link overlay”, the latter of which is just set to override the
/// underline attribute on the other two.
#[rustfmt::skip]
fn apply_overlay(mut base: Style, overlay: Style) -> Style {
if let Some(fg) = overlay.foreground { base.foreground = Some(fg); }
if let Some(bg) = overlay.background { base.background = Some(bg); }
@ -402,7 +423,6 @@ fn apply_overlay(mut base: Style, overlay: Style) -> Style {
}
// TODO: move this function to the ansiterm crate
#[cfg(test)]
#[cfg(unix)]
mod customs_test {
@ -419,7 +439,7 @@ mod customs_test {
$process_expected();
let definitions = Definitions {
ls: Some($ls.into()),
ls: Some($ls.into()),
exa: Some($exa.into()),
};
@ -431,13 +451,13 @@ mod customs_test {
($name:ident: ls $ls:expr, exa $exa:expr => exts $mappings:expr) => {
#[test]
fn $name() {
let mappings: Vec<(glob::Pattern, Style)>
= $mappings.iter()
.map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
.collect();
let mappings: Vec<(glob::Pattern, Style)> = $mappings
.iter()
.map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
.collect();
let definitions = Definitions {
ls: Some($ls.into()),
ls: Some($ls.into()),
exa: Some($exa.into()),
};
@ -451,13 +471,13 @@ mod customs_test {
let mut $expected = UiStyles::default();
$process_expected();
let mappings: Vec<(glob::Pattern, Style)>
= $mappings.iter()
.map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
.collect();
let mappings: Vec<(glob::Pattern, Style)> = $mappings
.iter()
.map(|t| (glob::Pattern::new(t.0).unwrap(), t.1))
.collect();
let definitions = Definitions {
ls: Some($ls.into()),
ls: Some($ls.into()),
exa: Some($exa.into()),
};
@ -469,7 +489,6 @@ mod customs_test {
};
}
// LS_COLORS can affect all of these colours:
test!(ls_di: ls "di=31", exa "" => colours c -> { c.filekinds.directory = Red.normal(); });
test!(ls_ex: ls "ex=32", exa "" => colours c -> { c.filekinds.executable = Green.normal(); });

View File

@ -2,7 +2,7 @@ use ansiterm::Style;
use crate::theme::lsc::Pair;
#[rustfmt::skip]
#[derive(Debug, Default, PartialEq)]
pub struct UiStyles {
pub colourful: bool,
@ -30,6 +30,7 @@ pub struct UiStyles {
pub broken_path_overlay: Style, // bO
}
#[rustfmt::skip]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct FileKinds {
pub normal: Style, // fi
@ -44,6 +45,7 @@ pub struct FileKinds {
pub mount_point: Style, // mp
}
#[rustfmt::skip]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Permissions {
pub user_read: Style, // ur
@ -65,6 +67,7 @@ pub struct Permissions {
pub attribute: Style, // xa
}
#[rustfmt::skip]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Size {
pub major: Style, // df
@ -83,6 +86,7 @@ pub struct Size {
pub unit_huge: Style, // sb ut
}
#[rustfmt::skip]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Users {
pub user_you: Style, // uu
@ -91,12 +95,14 @@ pub struct Users {
pub group_not_yours: Style, // gn
}
#[rustfmt::skip]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Links {
pub normal: Style, // lc
pub multi_link_file: Style, // lm
}
#[rustfmt::skip]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Git {
pub new: Style, // ga
@ -108,6 +114,7 @@ pub struct Git {
pub conflicted: Style, // gc
}
#[rustfmt::skip]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GitRepo {
pub branch_main: Style,
@ -125,6 +132,7 @@ pub struct SELinuxContext {
pub range: Style, // Sl
}
#[rustfmt::skip]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct SecurityContext {
pub none: Style, // Sn
@ -132,6 +140,7 @@ pub struct SecurityContext {
}
/// Drawing styles based on the type of file (video, image, compressed, etc)
#[rustfmt::skip]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct FileType {
pub image: Style, // im - image file
@ -152,13 +161,12 @@ impl UiStyles {
}
}
impl UiStyles {
/// Sets a value on this set of colours using one of the keys understood
/// by the `LS_COLORS` environment variable. Invalid keys set nothing, but
/// return false.
pub fn set_ls(&mut self, pair: &Pair<'_>) -> bool {
#[rustfmt::skip]
match pair.key {
"di" => self.filekinds.directory = pair.to_style(), // DIR
"ex" => self.filekinds.executable = pair.to_style(), // EXEC
@ -182,6 +190,7 @@ impl UiStyles {
/// but return false. This doesnt take the `LS_COLORS` keys into account,
/// so `set_ls` should have been run first.
pub fn set_exa(&mut self, pair: &Pair<'_>) -> bool {
#[rustfmt::skip]
match pair.key {
"ur" => self.perms.user_read = pair.to_style(),
"uw" => self.perms.user_write = pair.to_style(),

View File

@ -1,26 +1,22 @@
#[test]
fn cli_all_tests() {
trycmd::TestCases::new()
.case("tests/cmd/*_all.toml");
trycmd::TestCases::new().case("tests/cmd/*_all.toml");
}
#[test]
#[cfg(unix)]
fn cli_unix_tests() {
trycmd::TestCases::new()
.case("tests/cmd/*_unix.toml");
trycmd::TestCases::new().case("tests/cmd/*_unix.toml");
}
#[test]
#[cfg(windows)]
fn cli_windows_tests() {
trycmd::TestCases::new()
.case("tests/cmd/*_windows.toml");
trycmd::TestCases::new().case("tests/cmd/*_windows.toml");
}
#[test]
#[cfg(feature="nix")]
#[cfg(feature = "nix")]
fn cli_nix_tests() {
trycmd::TestCases::new()
.case("tests/cmd/*_nix.toml");
trycmd::TestCases::new().case("tests/cmd/*_nix.toml");
}