use crate::GLOBAL_CONFIG;
use crate::config::generate::{
PrintMode,
RawMode,
};
use crate::data::paths::{
BasicDirEntryInfo,
PathData,
};
use crate::library::results::{
HttmError,
HttmResult,
};
use lscolors::{
LsColors,
Style,
};
use nu_ansi_term::AnsiString;
use std::borrow::Cow;
use std::fs::FileType;
use std::io::Write;
use std::iter::Iterator;
use std::path::{
Path,
PathBuf,
};
use std::sync::LazyLock;
use std::time::SystemTime;
use time::{
OffsetDateTime,
UtcOffset,
format_description,
};
use unit_prefix::NumberPrefix;
use which::which;
pub fn pwd() -> HttmResult<PathBuf> {
let Ok(pwd) = std::env::current_dir() else {
return HttmError::new(
"Working directory does not exist or your do not have permissions to access it.",
)
.into();
};
Ok(pwd)
}
pub fn get_mount_command() -> HttmResult<PathBuf> {
which("mount").map_err(|_err| {
HttmError::new(
"'mount' command not be found. Make sure the command 'mount' is in your path.",
)
.into()
})
}
pub fn get_btrfs_command() -> HttmResult<PathBuf> {
which("btrfs").map_err(|_err| {
HttmError::new("'btrfs' command not found. Make sure the command 'btrfs' is in your path.")
.into()
})
}
pub fn user_has_effective_root(msg: &str) -> HttmResult<()> {
if !nix::unistd::geteuid().is_root() {
let err = format!("Superuser privileges are required to execute: {}.", msg);
return HttmError::new(&err).into();
}
Ok(())
}
pub fn delimiter() -> char {
if let PrintMode::Raw(RawMode::Zero) = GLOBAL_CONFIG.print_mode {
return '\0';
}
'\n'
}
pub fn make_tmp_path(path: &Path) -> PathBuf {
path.with_extension("tmp_httm")
}
pub fn find_common_path<I, P>(paths: I) -> Option<Box<Path>>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let mut path_iter = paths.into_iter();
let initial_value = path_iter.next()?.as_ref().to_path_buf();
path_iter
.try_fold(initial_value, |acc, path| cmp_path(acc, path))
.map(|res| res.into_boxed_path())
}
fn cmp_path<A: AsRef<Path>, B: AsRef<Path>>(a: A, b: B) -> Option<PathBuf> {
let a_components = a.as_ref().components();
let b_components = b.as_ref().components();
let common_path: PathBuf = a_components
.zip(b_components)
.take_while(|(a_path, b_path)| a_path == b_path)
.map(|(a_path, _b_path)| a_path)
.collect();
if common_path.components().count() > 1 {
Some(common_path)
} else {
None
}
}
pub fn print_output_buf(output_buf: &str) -> HttmResult<()> {
let out = std::io::stdout();
let mut out_locked = out.lock();
out_locked.write_all(output_buf.as_bytes())?;
out_locked.flush().map_err(std::convert::Into::into)
}
pub trait HttmIsDir {
fn file_type(&self) -> Result<FileType, std::io::Error>;
fn path(&self) -> &Path;
fn httm_is_dir<T>(&self) -> bool {
match self.file_type() {
Ok(file_type) => match file_type {
file_type if file_type.is_dir() => true,
file_type if file_type.is_file() => false,
file_type if file_type.is_symlink() => {
match self.path().read_link() {
Ok(link_target) if link_target.is_dir() => true,
Ok(_link_target) => false,
_ => false,
}
}
_ => false,
},
_ => false,
}
}
}
impl<T: AsRef<Path>> HttmIsDir for T {
fn file_type(&self) -> Result<FileType, std::io::Error> {
Ok(self.as_ref().symlink_metadata()?.file_type())
}
fn path(&self) -> &Path {
self.as_ref()
}
}
impl HttmIsDir for PathData {
fn file_type(&self) -> Result<FileType, std::io::Error> {
Ok(self
.opt_file_type()
.ok_or_else(|| std::io::ErrorKind::NotFound)?)
}
fn path(&self) -> &Path {
&self.path()
}
}
impl HttmIsDir for BasicDirEntryInfo {
fn file_type(&self) -> Result<FileType, std::io::Error> {
self.opt_filetype()
.copied()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))
}
fn path(&self) -> &Path {
self.path()
}
}
pub static ENV_LS_COLORS: LazyLock<LsColors> =
LazyLock::new(|| LsColors::from_env().unwrap_or_default());
pub static BASE_STYLE: LazyLock<nu_ansi_term::Style> =
LazyLock::new(|| nu_ansi_term::Style::default());
pub static PHANTOM_STYLE: LazyLock<nu_ansi_term::Style> = LazyLock::new(|| BASE_STYLE.dimmed());
pub trait PaintString<'a> {
fn ls_style(&self) -> Option<lscolors::style::Style>;
fn is_phantom(&self) -> bool;
fn display_name(&self) -> Cow<'_, str>;
fn paint_string(&'a self) -> AnsiString<'a> {
let display_name = self.display_name();
if self.is_phantom() {
return PHANTOM_STYLE.paint(display_name);
}
match self
.ls_style()
.map(|style| Style::to_nu_ansi_term_style(&style))
{
Some(ansi_style) => ansi_style.paint(display_name),
None => BASE_STYLE.paint(display_name),
}
}
}
impl<'a> PaintString<'a> for PathData {
fn ls_style(&self) -> Option<lscolors::style::Style> {
self.opt_style().copied()
}
fn is_phantom(&self) -> bool {
self.opt_path_metadata().is_none()
}
fn display_name(&self) -> Cow<'_, str> {
self.path().to_string_lossy()
}
}
impl<'a> PaintString<'a> for BasicDirEntryInfo {
fn ls_style(&self) -> Option<lscolors::style::Style> {
ENV_LS_COLORS.style_for(self).copied()
}
fn is_phantom(&self) -> bool {
self.opt_metadata().is_none()
}
fn display_name(&self) -> Cow<'_, str> {
let display_name = self.display_name();
match self.opt_filetype() {
Some(ft) if ft.is_file() => display_name,
Some(ft) if ft.is_dir() && display_name != "/" => {
let mut res = display_name.to_string();
res.push('/');
Cow::Owned(res)
}
Some(ft) if ft.is_symlink() => match std::fs::read_link(self.path()).ok() {
Some(link_target) => {
let link_name = format!(" -> {}", link_target.to_string_lossy());
let mut res = display_name.to_string();
res.push_str(&link_name);
Cow::Owned(res)
}
None => {
let mut res = display_name.to_string();
res.push_str(" -> ?");
Cow::Owned(res)
}
},
_ => display_name,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DateFormat {
Display,
Timestamp,
}
static DATE_FORMAT_DISPLAY: &str =
"[weekday repr:short] [month repr:short] [day] [hour]:[minute]:[second] [year]";
static DATE_FORMAT_TIMESTAMP: &str = "[year]-[month]-[day]-[hour]:[minute]:[second]";
pub fn date_string(
utc_offset: UtcOffset,
system_time: &SystemTime,
date_format: DateFormat,
) -> String {
let date_time: OffsetDateTime = (*system_time).into();
let parsed_format = format_description::parse(date_string_format(&date_format))
.expect("timestamp date format is invalid");
let raw_string = date_time
.to_offset(utc_offset)
.format(&parsed_format)
.expect("timestamp date format could not be applied to the date supplied");
if utc_offset == UtcOffset::UTC {
return match &date_format {
DateFormat::Timestamp => raw_string + "_UTC",
DateFormat::Display => raw_string + " UTC",
};
}
raw_string
}
fn date_string_format<'a>(format: &DateFormat) -> &'a str {
match format {
DateFormat::Display => DATE_FORMAT_DISPLAY,
DateFormat::Timestamp => DATE_FORMAT_TIMESTAMP,
}
}
pub fn display_human_size(size: u64) -> String {
let size = size as f64;
match NumberPrefix::binary(size) {
NumberPrefix::Standalone(bytes) => format!("{bytes} bytes"),
NumberPrefix::Prefixed(prefix, n) => format!("{n:.1} {prefix}B"),
}
}
pub fn is_metadata_same<P>(src: P, dst: P) -> HttmResult<()>
where
P: AsRef<Path>,
{
let src_pd = PathData::without_styling(src.as_ref(), None);
let dst_pd = PathData::without_styling(dst.as_ref(), None);
if src_pd.opt_path_metadata().is_none() {
let description = format!("Metadata not found: {:?}", src.as_ref());
return HttmError::from(description).into();
}
if dst_pd.opt_path_metadata().is_none() {
let description = format!("Metadata not found: {:?}", dst.as_ref());
return HttmError::from(description).into();
}
if src.as_ref().is_symlink() && (src.as_ref().read_link().ok() != dst.as_ref().read_link().ok())
{
let description = format!(
"Symlink targets do not match: {:?} -> {:?}",
src.as_ref(),
dst.as_ref()
);
return HttmError::from(description).into();
}
if src_pd.opt_path_metadata() != dst_pd.opt_path_metadata() {
let description = format!(
"Metadata mismatch: {:?}::{:?} !-> {:?}::{:?}",
src.path(),
src_pd.opt_path_metadata(),
dst.path(),
dst_pd.opt_path_metadata()
);
return HttmError::from(description).into();
}
Ok(())
}