From d27e259b26f25f0af56e398d361489e94b5d68e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Wed, 10 Nov 2021 09:51:26 -0300 Subject: [PATCH 1/5] Reorganizing src/utils, by renaming submodules And moving formatting stuff out of fs.rs --- src/commands.rs | 4 +- src/utils/{bytes.rs => formatting.rs} | 49 ++++++++++++++++- src/utils/fs.rs | 53 ++----------------- src/utils/mod.rs | 14 ++--- src/utils/{question_policy.rs => question.rs} | 2 + 5 files changed, 65 insertions(+), 57 deletions(-) rename src/utils/{bytes.rs => formatting.rs} (61%) rename src/utils/{question_policy.rs => question.rs} (97%) diff --git a/src/commands.rs b/src/commands.rs index 8de3b77..4ac4792 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -22,7 +22,7 @@ use crate::{ info, list::{self, ListOptions}, 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, }, 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") .detail(format!( "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") .hint("Provide a file with a supported extension:") diff --git a/src/utils/bytes.rs b/src/utils/formatting.rs similarity index 61% rename from src/utils/bytes.rs rename to src/utils/formatting.rs index f31cdd3..7eedc45 100644 --- a/src/utils/bytes.rs +++ b/src/utils/formatting.rs @@ -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 for a comparison. +pub fn to_utf(os_str: impl AsRef) -> 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 to comma separated String +/// +/// Panics if the slice is empty. +pub fn concatenate_os_str_list(os_strs: &[impl AsRef]) -> 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) -> 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. pub struct Bytes { diff --git a/src/utils/fs.rs b/src/utils/fs.rs index efa81be..4055995 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -1,26 +1,25 @@ -//! Random filesystem-related stuff used on ouch. +//! Filesystem utility functions. use std::{ - borrow::Cow, env, - ffi::OsStr, fs::ReadDir, io::Read, - path::{Component, Path, PathBuf}, + path::{Path, PathBuf}, }; use fs_err as fs; +use super::to_utf; 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 { let is_empty = |mut rd: ReadDir| rd.next().is_none(); 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<()> { if !path.exists() { fs::create_dir_all(path)?; @@ -29,13 +28,6 @@ pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> { 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 /// one that contains the file pointed to by `filename`. pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result { @@ -47,41 +39,6 @@ pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result { 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) -> String { - let text = format!("{:?}", os_str.as_ref()); - text.trim_matches('"').to_string() -} - -/// Converts a slice of AsRef to comma separated String -/// -/// Panics if the slice is empty. -pub fn concatenate_list_of_os_str(os_strs: &[impl AsRef]) -> 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) -> 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 /// Source: https://en.wikipedia.org/wiki/List_of_file_signatures pub fn try_infer_extension(path: &Path) -> Option { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 8b147bf..c0c99ec 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,9 +1,11 @@ -//! Random filesystem-related stuff used on ouch. +//! Random and miscellaneous utils used in ouch. -mod bytes; +mod formatting; mod fs; -mod question_policy; +mod question; -pub use bytes::Bytes; -pub use fs::*; -pub use question_policy::*; +pub use formatting::{concatenate_os_str_list, nice_directory_display, strip_cur_dir, to_utf, Bytes}; +pub use fs::{cd_into_same_dir_as, colors, create_dir_if_non_existent, dir_is_empty, try_infer_extension}; +pub use question::{ + create_or_ask_overwrite, user_wants_to_continue_decompressing, user_wants_to_overwrite, QuestionPolicy, +}; diff --git a/src/utils/question_policy.rs b/src/utils/question.rs similarity index 97% rename from src/utils/question_policy.rs rename to src/utils/question.rs index fe92d1a..0cf779e 100644 --- a/src/utils/question_policy.rs +++ b/src/utils/question.rs @@ -1,3 +1,5 @@ +//! Utils related to asking [Y/n] questions to the user. + use std::path::Path; use fs_err as fs; From 63cfeb26ef608502977c5489aae8b1473ec9843b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Wed, 10 Nov 2021 09:52:05 -0300 Subject: [PATCH 2/5] Add accidentally removed link in docs --- src/utils/formatting.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/formatting.rs b/src/utils/formatting.rs index 7eedc45..db2aef4 100644 --- a/src/utils/formatting.rs +++ b/src/utils/formatting.rs @@ -9,7 +9,7 @@ use std::{ /// /// This is different from [`Path::display`]. /// -/// See for a comparison. +/// See https://gist.github.com/marcospb19/ebce5572be26397cf08bbd0fd3b65ac1 for a comparison. pub fn to_utf(os_str: impl AsRef) -> String { let text = format!("{:?}", os_str.as_ref()); text.trim_matches('"').to_string() From a531d44e2b4add69f4a7bc505310994051de61d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Wed, 10 Nov 2021 09:55:31 -0300 Subject: [PATCH 3/5] Move colors module to it's own file --- src/utils/colors.rs | 36 ++++++++++++++++++++++++++++++++++++ src/utils/fs.rs | 37 ------------------------------------- src/utils/mod.rs | 3 ++- 3 files changed, 38 insertions(+), 38 deletions(-) create mode 100644 src/utils/colors.rs diff --git a/src/utils/colors.rs b/src/utils/colors.rs new file mode 100644 index 0000000..ebe6c7d --- /dev/null +++ b/src/utils/colors.rs @@ -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 = 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"); diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 4055995..875619e 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -103,40 +103,3 @@ pub fn try_infer_extension(path: &Path) -> Option { 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 = 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"); -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index c0c99ec..edb97c8 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,11 +1,12 @@ //! Random and miscellaneous utils used in ouch. +pub mod colors; mod formatting; mod fs; mod question; pub use formatting::{concatenate_os_str_list, nice_directory_display, strip_cur_dir, to_utf, Bytes}; -pub use fs::{cd_into_same_dir_as, colors, create_dir_if_non_existent, dir_is_empty, try_infer_extension}; +pub use fs::{cd_into_same_dir_as, create_dir_if_non_existent, dir_is_empty, try_infer_extension}; pub use question::{ create_or_ask_overwrite, user_wants_to_continue_decompressing, user_wants_to_overwrite, QuestionPolicy, }; From f09f1cecba153da525e46746b2a1253b9a97b485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Wed, 10 Nov 2021 09:57:51 -0300 Subject: [PATCH 4/5] Merging dialogs.rs with question.rs --- src/dialogs.rs | 56 ------------------------------------------- src/lib.rs | 1 - src/utils/question.rs | 56 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 59 deletions(-) delete mode 100644 src/dialogs.rs diff --git a/src/dialogs.rs b/src/dialogs.rs deleted file mode 100644 index dbf6a73..0000000 --- a/src/dialogs.rs +++ /dev/null @@ -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 { - 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 - } - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 137775e..5644bf4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,6 @@ pub mod macros; pub mod archive; pub mod cli; pub mod commands; -pub mod dialogs; pub mod error; pub mod extension; pub mod list; diff --git a/src/utils/question.rs b/src/utils/question.rs index 0cf779e..01bff85 100644 --- a/src/utils/question.rs +++ b/src/utils/question.rs @@ -1,13 +1,20 @@ //! Utils related to asking [Y/n] questions to the user. +//! +//! Example: +//! "Do you want to overwrite 'archive.tar.gz'? [Y/n]" -use std::path::Path; +use std::{ + borrow::Cow, + io::{self, Write}, + path::Path, +}; use fs_err as fs; use super::{strip_cur_dir, to_utf}; use crate::{ - dialogs::Confirmation, error::{Error, Result}, + utils::colors, }; #[derive(Debug, PartialEq, Clone, Copy)] @@ -69,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 { + 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 + } + } + } +} From b7c6589864f4dd5b9257b3e21359c6ae26d58b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Wed, 10 Nov 2021 10:09:54 -0300 Subject: [PATCH 5/5] Small docs update --- src/lib.rs | 2 +- src/utils/mod.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 5644bf4..9fa58cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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)] diff --git a/src/utils/mod.rs b/src/utils/mod.rs index edb97c8..1558478 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,7 @@ //! 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. pub mod colors; mod formatting;