mirror of
https://github.com/eza-community/eza
synced 2024-07-01 07:24:50 +00:00
refactor: fix rustfmt issues and place skips where needed
Signed-off-by: Sandro-Alessio Gierens <sandro@gierens.de>
This commit is contained in:
parent
54c8dae733
commit
f555d42972
|
@ -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);
|
||||
|
|
58
build.rs
58
build.rs
|
@ -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 project’s 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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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 that’s
|
||||
/// 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 can’t be read, or
|
||||
/// isn’t actually a directory, or if there’s 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 aren’t 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,11 @@ use log::*;
|
|||
|
||||
use crate::fs::fields as f;
|
||||
|
||||
|
||||
/// A **Git cache** is assembled based on the user’s input arguments.
|
||||
///
|
||||
/// This uses vectors to avoid the overhead of hashing: it’s 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 we’ve 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 repository’s 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 we’ve extracted from the repository, but only after we’ve
|
||||
/// 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 hasn’t 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 doesn’t make it
|
||||
// look any faster.
|
||||
|
||||
|
||||
/// Container of Git statuses for all the files in this folder’s 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
|
||||
/// you’d ask a repo about “./README.md” but it only knows about
|
||||
|
@ -323,8 +327,8 @@ fn reorient(path: &Path) -> PathBuf {
|
|||
|
||||
// TODO: I’m 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);
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -29,7 +29,6 @@ pub type time_t = i64;
|
|||
/// The type of a file’s user ID.
|
||||
pub type uid_t = u32;
|
||||
|
||||
|
||||
/// The file’s base type, which gets displayed in the very first column of the
|
||||
/// details output.
|
||||
///
|
||||
|
@ -56,9 +55,9 @@ impl Type {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// The file’s 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 file’s 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 file’s 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 file’s 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 file’s status in a Git repository. Whether a file is in a repository or
|
||||
/// not is handled by the Git module, rather than having a “null” variant in
|
||||
/// this enum.
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
pub enum GitStatus {
|
||||
|
||||
/// This file hasn’t changed since the last commit.
|
||||
NotModified,
|
||||
|
||||
|
@ -235,18 +224,16 @@ pub enum GitStatus {
|
|||
Conflicted,
|
||||
}
|
||||
|
||||
|
||||
/// A file’s complete Git status. It’s possible to make changes to a file, add
|
||||
/// it to the staging area, then make *more* changes, so we need to list each
|
||||
/// file’s 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),
|
||||
|
|
278
src/fs/file.rs
278
src/fs/file.rs
|
@ -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 Rust’s `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 file’s 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 file’s 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 file’s 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 file’s 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 file’s ‘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 file’s 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 isn’t a link to begin with, but also if, say, we don’t 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 doesn’t lead to a file, for whatever reason. This
|
||||
/// gets used to determine how to highlight the link in grid views.
|
||||
pub fn is_broken(&self) -> bool {
|
||||
|
@ -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;
|
||||
|
|
|
@ -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 don’t 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 {
|
||||
|
||||
/// Don’t apply any sorting. This is usually used as an optimisation in
|
||||
/// scripts, where the order doesn’t 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 isn’t 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
|
||||
/// don’t 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 it’s 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 won’t 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"));
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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. It’s 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. It’s 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
|
||||
|
|
|
@ -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 there’s no extension, either!
|
||||
} else {
|
||||
vec![] // No source files if there’s no extension, either!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
172
src/main.rs
172
src/main.rs
|
@ -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 won’t 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.
|
||||
|
|
|
@ -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 wouldn’t 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`
|
||||
/// flag’s 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'))));
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 argument’s 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, doesn’t 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 {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// I’ve gone back and forth between whether to sort case-sensitively or
|
||||
// insensitively by default. The default string sort in most programming
|
||||
// languages takes each character’s 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` argument’s 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 doesn’t
|
||||
// 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::*;
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 user’s
|
||||
/// 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 isn’t passed
|
||||
assert!(!matches!(opts, OptionsResult::Help(_))) // no help when --help isn’t passed
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,12 +68,11 @@
|
|||
//! --grid --long` shouldn’t complain about `--long` being given twice when
|
||||
//! it’s 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
|
||||
/// user’s 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 they’ve 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};
|
||||
|
|
|
@ -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 doesn’t 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 there’s 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 user’s 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
|
||||
// doesn’t exist.
|
||||
if ! parsing {
|
||||
if !parsing {
|
||||
frees.push(arg);
|
||||
}
|
||||
else if arg == "--" {
|
||||
} else if arg == "--" {
|
||||
parsing = false;
|
||||
}
|
||||
|
||||
// If the string starts with *two* dashes then it’s 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 there’s 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 it’s one or more
|
||||
// short arguments.
|
||||
else if bytes.starts_with(b"-") && arg != "-" {
|
||||
|
@ -234,15 +226,15 @@ impl Args {
|
|||
// it’s an error if any of the first set of arguments actually
|
||||
// takes a value.
|
||||
if let Some((before, after)) = split_on_equals(short_arg) {
|
||||
let (arg_with_value, other_args) = 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 there’s 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, it’s 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 user’s command-line strings.
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Matches<'args> {
|
||||
|
||||
/// The flags that were parsed from the user’s 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 user’s 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 wasn’t, and an error in
|
||||
/// strict mode if it was specified more than once.
|
||||
|
@ -382,16 +372,22 @@ impl<'a> MatchedFlags<'a> {
|
|||
///
|
||||
/// You’ll 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> {
|
|||
///
|
||||
/// You’ll 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> {
|
|||
///
|
||||
/// It’s 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 user’s input that meant it couldn’t 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 isn’t even used
|
||||
let strictness = Strictness::UseLastArguments; // this isn’t 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 isn’t even used
|
||||
let strictness = Strictness::UseLastArguments; // this isn’t 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());
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 aren’t anymore!
|
||||
|
||||
impl VersionString {
|
||||
|
||||
/// Determines how to show the version, if at all, based on the user’s
|
||||
/// 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(_)));
|
||||
}
|
||||
|
|
|
@ -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 user’s 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 hasn’t been passed, then check if we need to warn the
|
||||
// user about flags that won’t 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 user’s 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 file’s time fields should be displayed for it
|
||||
/// based on the user’s 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, they’ll 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 doesn’t 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(_)));
|
||||
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// I’d 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.
|
||||
///
|
||||
/// It’s 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;
|
||||
|
|
|
@ -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 can’t 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() doesn’t quite seem like the right name for
|
||||
// the style that’s 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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 file’s 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 they’re
|
||||
/// a broken link or can’t 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 {
|
||||
|
||||
/// Don’t 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 we’re 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 it’s 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 it’s not! In a grid view, where there’s 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
|
||||
// link’s filename as the link colour.
|
||||
if !self.file.name.is_empty() {
|
||||
// The “missing file” colour seems like it should be used here,
|
||||
// but it’s not! In a grid view, where there’s 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
|
||||
// link’s 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 symlink’s target, up to but not
|
||||
/// including the file’s 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 don’t
|
||||
/// 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 don’t
|
||||
/// 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()
|
||||
}
|
||||
|
|
|
@ -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 isn’t *quite* the same as the lines view, which also
|
||||
// displays full link paths.
|
||||
|
|
|
@ -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 wouldn’t 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 that’s 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 that’s 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 column’s 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 we’ve figured out how many columns can fit in the user’s terminal,
|
||||
// and it turns out there aren’t 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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>>,
|
||||
|
|
|
@ -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 we’re only interested in stdout because it’s
|
||||
// where the output goes.
|
||||
|
||||
#[rustfmt::skip]
|
||||
match self {
|
||||
Self::Set(width) => Some(width),
|
||||
Self::Automatic => terminal_size::terminal_size().map(|(w, _)| w.0.into()),
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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 they’re
|
||||
// all going to be one character wide, and don’t need to compute the
|
||||
// cell’s 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())
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("-")
|
||||
};
|
||||
|
|
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 file’s time fields. These three fields are standard
|
||||
/// across most (all?) operating systems.
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum TimeType {
|
||||
|
||||
/// The file’s modified time (`st_mtime`).
|
||||
Modified,
|
||||
|
||||
|
@ -281,9 +270,9 @@ pub enum TimeType {
|
|||
}
|
||||
|
||||
impl TimeType {
|
||||
|
||||
/// Returns the text to use for a column’s 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 file’s time fields should be displayed in the
|
||||
/// columns output.
|
||||
///
|
||||
/// There should always be at least one of these — there’s 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 user’s computer’s configuration.
|
||||
///
|
||||
/// Any environment field should be able to be mocked up for test runs.
|
||||
pub struct Environment {
|
||||
|
||||
/// The computer’s 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 {
|
||||
|
|
|
@ -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 user’s 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,10 +38,8 @@
|
|||
//! successfully `stat`ted, we don’t 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. It’s
|
||||
/// 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 isn’t 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();
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 isn’t 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), they’re 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(); });
|
||||
|
|
|
@ -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 doesn’t 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(),
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user