Merge pull request #179 from ouch-org/organizing-utils

Organizing utils
This commit is contained in:
João Marcos Bezerra 2021-11-10 10:12:28 -03:00 committed by GitHub
commit fe6913118d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 160 additions and 154 deletions

View File

@ -22,7 +22,7 @@ use crate::{
info, info,
list::{self, ListOptions}, list::{self, ListOptions},
utils::{ utils::{
self, concatenate_list_of_os_str, dir_is_empty, nice_directory_display, to_utf, try_infer_extension, self, concatenate_os_str_list, dir_is_empty, nice_directory_display, to_utf, try_infer_extension,
user_wants_to_continue_decompressing, user_wants_to_continue_decompressing,
}, },
warning, Opts, QuestionPolicy, Subcommand, warning, Opts, QuestionPolicy, Subcommand,
@ -189,7 +189,7 @@ pub fn run(args: Opts, question_policy: QuestionPolicy) -> crate::Result<()> {
let error = FinalError::with_title("Cannot decompress files without extensions") let error = FinalError::with_title("Cannot decompress files without extensions")
.detail(format!( .detail(format!(
"Files without supported extensions: {}", "Files without supported extensions: {}",
concatenate_list_of_os_str(&files_missing_format) concatenate_os_str_list(&files_missing_format)
)) ))
.detail("Decompression formats are detected automatically by the file extension") .detail("Decompression formats are detected automatically by the file extension")
.hint("Provide a file with a supported extension:") .hint("Provide a file with a supported extension:")

View File

@ -1,56 +0,0 @@
//! Pretty (and colored) dialog for asking [Y/n] for the end user.
//!
//! Example:
//! "Do you want to overwrite 'archive.tar.gz'? [Y/n]"
use std::{
borrow::Cow,
io::{self, Write},
};
use crate::utils::colors;
/// Confirmation dialog for end user with [Y/n] question.
///
/// If the placeholder is found in the prompt text, it will be replaced to form the final message.
pub struct Confirmation<'a> {
/// The message to be displayed with the placeholder text in it.
/// e.g.: "Do you want to overwrite 'FILE'?"
pub prompt: &'a str,
/// The placeholder text that will be replaced in the `ask` function:
/// e.g.: Some("FILE")
pub placeholder: Option<&'a str>,
}
impl<'a> Confirmation<'a> {
/// Creates a new Confirmation.
pub const fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self {
Self { prompt, placeholder: pattern }
}
/// Creates user message and receives a boolean input to be used on the program
pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result<bool> {
let message = match (self.placeholder, substitute) {
(None, _) => Cow::Borrowed(self.prompt),
(Some(_), None) => unreachable!("dev error, should be reported, we checked this won't happen"),
(Some(placeholder), Some(subs)) => Cow::Owned(self.prompt.replace(placeholder, subs)),
};
// Ask the same question to end while no valid answers are given
loop {
print!("{} [{}Y{}/{}n{}] ", message, *colors::GREEN, *colors::RESET, *colors::RED, *colors::RESET);
io::stdout().flush()?;
let mut answer = String::new();
io::stdin().read_line(&mut answer)?;
answer.make_ascii_lowercase();
match answer.trim() {
"" | "y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => continue, // Try again
}
}
}
}

View File

@ -1,4 +1,4 @@
//! This library isn't meant to be published, but used internally by our binary crate `main.rs`. //! This library is just meant to supply needs for the `ouch` binary crate.
#![warn(missing_docs)] #![warn(missing_docs)]
@ -8,7 +8,6 @@ pub mod macros;
pub mod archive; pub mod archive;
pub mod cli; pub mod cli;
pub mod commands; pub mod commands;
pub mod dialogs;
pub mod error; pub mod error;
pub mod extension; pub mod extension;
pub mod list; pub mod list;

36
src/utils/colors.rs Normal file
View File

@ -0,0 +1,36 @@
//! Colored output in ouch with bright colors.
#![allow(dead_code)]
use std::env;
use once_cell::sync::Lazy;
static DISABLE_COLORED_TEXT: Lazy<bool> = Lazy::new(|| {
env::var_os("NO_COLOR").is_some() || atty::isnt(atty::Stream::Stdout) || atty::isnt(atty::Stream::Stderr)
});
macro_rules! color {
($name:ident = $value:literal) => {
#[cfg(target_family = "unix")]
/// Inserts color onto text based on configuration
pub static $name: Lazy<&str> = Lazy::new(|| if *DISABLE_COLORED_TEXT { "" } else { $value });
#[cfg(not(target_family = "unix"))]
pub static $name: &&str = &"";
};
}
color!(RESET = "\u{1b}[39m");
color!(BLACK = "\u{1b}[38;5;8m");
color!(BLUE = "\u{1b}[38;5;12m");
color!(CYAN = "\u{1b}[38;5;14m");
color!(GREEN = "\u{1b}[38;5;10m");
color!(MAGENTA = "\u{1b}[38;5;13m");
color!(RED = "\u{1b}[38;5;9m");
color!(WHITE = "\u{1b}[38;5;15m");
color!(YELLOW = "\u{1b}[38;5;11m");
// Requires true color support
color!(ORANGE = "\u{1b}[38;2;255;165;0m");
color!(STYLE_BOLD = "\u{1b}[1m");
color!(STYLE_RESET = "\u{1b}[0m");
color!(ALL_RESET = "\u{1b}[0;39m");

View File

@ -1,4 +1,51 @@
use std::cmp; use std::{
borrow::Cow,
cmp,
ffi::OsStr,
path::{Component, Path},
};
/// Converts an OsStr to utf8 with custom formatting.
///
/// This is different from [`Path::display`].
///
/// See https://gist.github.com/marcospb19/ebce5572be26397cf08bbd0fd3b65ac1 for a comparison.
pub fn to_utf(os_str: impl AsRef<OsStr>) -> String {
let text = format!("{:?}", os_str.as_ref());
text.trim_matches('"').to_string()
}
/// Removes the current dir from the beginning of a path
/// normally used for presentation sake.
/// If this function fails, it will return source path as a PathBuf.
pub fn strip_cur_dir(source_path: &Path) -> &Path {
source_path.strip_prefix(Component::CurDir).unwrap_or(source_path)
}
/// Converts a slice of AsRef<OsStr> to comma separated String
///
/// Panics if the slice is empty.
pub fn concatenate_os_str_list(os_strs: &[impl AsRef<OsStr>]) -> String {
let mut iter = os_strs.iter().map(AsRef::as_ref);
let mut string = to_utf(iter.next().unwrap()); // May panic
for os_str in iter {
string += ", ";
string += &to_utf(os_str);
}
string
}
/// Display the directory name, but change to "current directory" when necessary.
pub fn nice_directory_display(os_str: impl AsRef<OsStr>) -> Cow<'static, str> {
if os_str.as_ref() == "." {
Cow::Borrowed("current directory")
} else {
let text = to_utf(os_str);
Cow::Owned(format!("'{}'", text))
}
}
/// Struct useful to printing bytes as kB, MB, GB, etc. /// Struct useful to printing bytes as kB, MB, GB, etc.
pub struct Bytes { pub struct Bytes {

View File

@ -1,26 +1,25 @@
//! Random filesystem-related stuff used on ouch. //! Filesystem utility functions.
use std::{ use std::{
borrow::Cow,
env, env,
ffi::OsStr,
fs::ReadDir, fs::ReadDir,
io::Read, io::Read,
path::{Component, Path, PathBuf}, path::{Path, PathBuf},
}; };
use fs_err as fs; use fs_err as fs;
use super::to_utf;
use crate::{extension::Extension, info}; use crate::{extension::Extension, info};
/// Checks given path points to an empty directory. /// Checks if given path points to an empty directory.
pub fn dir_is_empty(dir_path: &Path) -> bool { pub fn dir_is_empty(dir_path: &Path) -> bool {
let is_empty = |mut rd: ReadDir| rd.next().is_none(); let is_empty = |mut rd: ReadDir| rd.next().is_none();
dir_path.read_dir().map(is_empty).unwrap_or_default() dir_path.read_dir().map(is_empty).unwrap_or_default()
} }
/// Creates the dir if non existent. /// Creates a directory at the path, if there is nothing there.
pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> { pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> {
if !path.exists() { if !path.exists() {
fs::create_dir_all(path)?; fs::create_dir_all(path)?;
@ -29,13 +28,6 @@ pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> {
Ok(()) Ok(())
} }
/// Removes the current dir from the beginning of a path
/// normally used for presentation sake.
/// If this function fails, it will return source path as a PathBuf.
pub fn strip_cur_dir(source_path: &Path) -> &Path {
source_path.strip_prefix(Component::CurDir).unwrap_or(source_path)
}
/// Returns current directory, but before change the process' directory to the /// Returns current directory, but before change the process' directory to the
/// one that contains the file pointed to by `filename`. /// one that contains the file pointed to by `filename`.
pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result<PathBuf> { pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result<PathBuf> {
@ -47,41 +39,6 @@ pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result<PathBuf> {
Ok(previous_location) Ok(previous_location)
} }
/// Converts an OsStr to utf8 with custom formatting.
///
/// This is different from [`Path::display`].
///
/// See for a comparison.
pub fn to_utf(os_str: impl AsRef<OsStr>) -> String {
let text = format!("{:?}", os_str.as_ref());
text.trim_matches('"').to_string()
}
/// Converts a slice of AsRef<OsStr> to comma separated String
///
/// Panics if the slice is empty.
pub fn concatenate_list_of_os_str(os_strs: &[impl AsRef<OsStr>]) -> String {
let mut iter = os_strs.iter().map(AsRef::as_ref);
let mut string = to_utf(iter.next().unwrap()); // May panic
for os_str in iter {
string += ", ";
string += &to_utf(os_str);
}
string
}
/// Display the directory name, but change to "current directory" when necessary.
pub fn nice_directory_display(os_str: impl AsRef<OsStr>) -> Cow<'static, str> {
if os_str.as_ref() == "." {
Cow::Borrowed("current directory")
} else {
let text = to_utf(os_str);
Cow::Owned(format!("'{}'", text))
}
}
/// Try to detect the file extension by looking for known magic strings /// Try to detect the file extension by looking for known magic strings
/// Source: https://en.wikipedia.org/wiki/List_of_file_signatures /// Source: https://en.wikipedia.org/wiki/List_of_file_signatures
pub fn try_infer_extension(path: &Path) -> Option<Extension> { pub fn try_infer_extension(path: &Path) -> Option<Extension> {
@ -146,40 +103,3 @@ pub fn try_infer_extension(path: &Path) -> Option<Extension> {
None None
} }
} }
/// Module with a list of bright colors.
#[allow(dead_code)]
pub mod colors {
use std::env;
use once_cell::sync::Lazy;
static DISABLE_COLORED_TEXT: Lazy<bool> = Lazy::new(|| {
env::var_os("NO_COLOR").is_some() || atty::isnt(atty::Stream::Stdout) || atty::isnt(atty::Stream::Stderr)
});
macro_rules! color {
($name:ident = $value:literal) => {
#[cfg(target_family = "unix")]
/// Inserts color onto text based on configuration
pub static $name: Lazy<&str> = Lazy::new(|| if *DISABLE_COLORED_TEXT { "" } else { $value });
#[cfg(not(target_family = "unix"))]
pub static $name: &&str = &"";
};
}
color!(RESET = "\u{1b}[39m");
color!(BLACK = "\u{1b}[38;5;8m");
color!(BLUE = "\u{1b}[38;5;12m");
color!(CYAN = "\u{1b}[38;5;14m");
color!(GREEN = "\u{1b}[38;5;10m");
color!(MAGENTA = "\u{1b}[38;5;13m");
color!(RED = "\u{1b}[38;5;9m");
color!(WHITE = "\u{1b}[38;5;15m");
color!(YELLOW = "\u{1b}[38;5;11m");
// Requires true color support
color!(ORANGE = "\u{1b}[38;2;255;165;0m");
color!(STYLE_BOLD = "\u{1b}[1m");
color!(STYLE_RESET = "\u{1b}[0m");
color!(ALL_RESET = "\u{1b}[0;39m");
}

View File

@ -1,9 +1,15 @@
//! Random filesystem-related stuff used on ouch. //! Random and miscellaneous utils used in ouch.
//!
//! In here we have the logic for custom formatting, some file and directory utils, and user
//! stdin interaction helpers.
mod bytes; pub mod colors;
mod formatting;
mod fs; mod fs;
mod question_policy; mod question;
pub use bytes::Bytes; pub use formatting::{concatenate_os_str_list, nice_directory_display, strip_cur_dir, to_utf, Bytes};
pub use fs::*; pub use fs::{cd_into_same_dir_as, create_dir_if_non_existent, dir_is_empty, try_infer_extension};
pub use question_policy::*; pub use question::{
create_or_ask_overwrite, user_wants_to_continue_decompressing, user_wants_to_overwrite, QuestionPolicy,
};

View File

@ -1,11 +1,20 @@
use std::path::Path; //! Utils related to asking [Y/n] questions to the user.
//!
//! Example:
//! "Do you want to overwrite 'archive.tar.gz'? [Y/n]"
use std::{
borrow::Cow,
io::{self, Write},
path::Path,
};
use fs_err as fs; use fs_err as fs;
use super::{strip_cur_dir, to_utf}; use super::{strip_cur_dir, to_utf};
use crate::{ use crate::{
dialogs::Confirmation,
error::{Error, Result}, error::{Error, Result},
utils::colors,
}; };
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
@ -67,3 +76,48 @@ pub fn user_wants_to_continue_decompressing(path: &Path, question_policy: Questi
} }
} }
} }
/// Confirmation dialog for end user with [Y/n] question.
///
/// If the placeholder is found in the prompt text, it will be replaced to form the final message.
pub struct Confirmation<'a> {
/// The message to be displayed with the placeholder text in it.
/// e.g.: "Do you want to overwrite 'FILE'?"
pub prompt: &'a str,
/// The placeholder text that will be replaced in the `ask` function:
/// e.g.: Some("FILE")
pub placeholder: Option<&'a str>,
}
impl<'a> Confirmation<'a> {
/// Creates a new Confirmation.
pub const fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self {
Self { prompt, placeholder: pattern }
}
/// Creates user message and receives a boolean input to be used on the program
pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result<bool> {
let message = match (self.placeholder, substitute) {
(None, _) => Cow::Borrowed(self.prompt),
(Some(_), None) => unreachable!("dev error, should be reported, we checked this won't happen"),
(Some(placeholder), Some(subs)) => Cow::Owned(self.prompt.replace(placeholder, subs)),
};
// Ask the same question to end while no valid answers are given
loop {
print!("{} [{}Y{}/{}n{}] ", message, *colors::GREEN, *colors::RESET, *colors::RED, *colors::RESET);
io::stdout().flush()?;
let mut answer = String::new();
io::stdin().read_line(&mut answer)?;
answer.make_ascii_lowercase();
match answer.trim() {
"" | "y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => continue, // Try again
}
}
}
}