use std::{
fmt,
io::{BufWriter, Write, stdout},
path::{Path, PathBuf},
};
use self::tree::Tree;
use crate::{Result, accessible::is_running_in_accessible_mode, utils::PathFmt};
#[derive(Debug, Clone, Copy)]
pub struct ListOptions {
pub tree: bool,
pub quiet: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListFileType {
File,
Directory,
Symlink { target: PathBuf },
Hardlink { target: PathBuf },
}
#[derive(Debug, Clone)]
pub struct FileInArchive {
pub path: PathBuf,
pub file_type: ListFileType,
}
pub fn list_files(
archive: &Path,
files: impl IntoIterator<Item = Result<FileInArchive>>,
list_options: ListOptions,
) -> Result<()> {
let mut out = BufWriter::new(stdout().lock());
if !list_options.quiet {
let _ = writeln!(out, "Archive: {}", PathFmt(archive));
}
if list_options.tree {
let tree = files.into_iter().collect::<Result<Tree>>()?;
tree.print(&mut out);
} else {
for file in files {
let FileInArchive { path, file_type } = file?;
print_entry(&mut out, path.display(), &file_type, list_options.quiet);
}
}
Ok(())
}
fn print_entry(out: &mut impl Write, name: impl fmt::Display, file_type: &ListFileType, quiet: bool) {
use crate::utils::colors::*;
match file_type {
ListFileType::File => {
let _ = writeln!(out, "{name}");
}
ListFileType::Symlink { target } | ListFileType::Hardlink { target } => {
if quiet {
let _ = writeln!(out, "{}{name}{}", *CYAN, *ALL_RESET);
return;
}
let suffix = if matches!(file_type, ListFileType::Hardlink { .. }) {
" (hardlink)"
} else {
""
};
if is_running_in_accessible_mode() {
let _ = writeln!(out, "{name} -> {}{suffix}", target.display());
} else {
let _ = writeln!(
out,
"{c}{name}{r} {c}-> {c}{target}{suffix}{r}",
c = *CYAN,
r = *ALL_RESET,
target = target.display()
);
}
}
ListFileType::Directory => {
let name_str = name.to_string();
let display_name = name_str.strip_suffix('/').unwrap_or(&name_str);
let output = if *DISABLE_COLORED_TEXT || is_running_in_accessible_mode() {
format!("{display_name}/")
} else {
format!("{}{}{}{}", *BLUE, *STYLE_BOLD, display_name, *ALL_RESET)
};
let _ = writeln!(out, "{output}");
}
}
}
mod tree {
use std::{
ffi::{OsStr, OsString},
io::Write,
path,
};
use bstr::{ByteSlice, ByteVec};
use indexmap::IndexMap;
use super::{FileInArchive, ListFileType};
use crate::{utils::NoQuotePathFmt, warning};
#[derive(Debug, Default)]
pub struct Tree {
file: Option<FileInArchive>,
children: IndexMap<OsString, Tree>,
}
impl Tree {
pub fn insert(&mut self, file: FileInArchive) {
self.insert_(file.clone(), file.path.iter());
}
fn insert_(&mut self, file: FileInArchive, mut path: path::Iter) {
if let Some(part) = path.next() {
if let Some(t) = self.children.get_mut(part) {
t.insert_(file, path)
} else {
let mut child = Self::default();
child.insert_(file, path);
self.children.insert(part.to_os_string(), child);
}
} else {
match &self.file {
None => self.file = Some(file),
Some(file) => {
warning!(
"multiple files with the same name in a single directory ({})",
NoQuotePathFmt(&file.path),
);
}
}
}
}
pub fn print(&self, out: &mut impl Write) {
for (i, (name, subtree)) in self.children.iter().enumerate() {
subtree.print_(out, name, "", i == self.children.len() - 1);
}
}
fn print_(&self, out: &mut impl Write, name: &OsStr, prefix: &str, last: bool) {
let final_part = match last {
true => draw::FINAL_LAST,
false => draw::FINAL_BRANCH,
};
let _ = write!(out, "{prefix}{final_part}");
let file_type = match &self.file {
Some(FileInArchive { file_type, .. }) => file_type.clone(),
None => ListFileType::Directory,
};
super::print_entry(
out,
<Vec<u8> as ByteVec>::from_os_str_lossy(name).as_bstr(),
&file_type,
false, );
let mut prefix = prefix.to_owned();
prefix.push_str(match last {
true => draw::PREFIX_EMPTY,
false => draw::PREFIX_LINE,
});
for (i, (name, subtree)) in self.children.iter().enumerate() {
subtree.print_(out, name, &prefix, i == self.children.len() - 1);
}
}
}
impl FromIterator<FileInArchive> for Tree {
fn from_iter<I: IntoIterator<Item = FileInArchive>>(iter: I) -> Self {
let mut tree = Self::default();
for file in iter {
tree.insert(file);
}
tree
}
}
mod draw {
pub const PREFIX_EMPTY: &str = " ";
pub const PREFIX_LINE: &str = "│ ";
pub const FINAL_LAST: &str = "└── ";
pub const FINAL_BRANCH: &str = "├── ";
}
}