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,
}
#[derive(Debug, Clone)]
pub struct FileInArchive {
pub path: PathBuf,
pub is_dir: bool,
}
pub fn list_files(
archive: &Path,
files: impl IntoIterator<Item = Result<FileInArchive>>,
list_options: ListOptions,
) -> Result<()> {
let mut out = BufWriter::new(stdout().lock());
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, is_dir } = file?;
print_entry(&mut out, path.display(), is_dir);
}
}
Ok(())
}
fn print_entry(out: &mut impl Write, name: impl fmt::Display, is_dir: bool) {
use crate::utils::colors::*;
if !is_dir {
let _ = writeln!(out, "{name}");
return;
}
let name_str = name.to_string();
let display_name = name_str.strip_suffix('/').unwrap_or(&name_str);
let output = if BLUE.is_empty() {
format!("{display_name}/")
} else if is_running_in_accessible_mode() {
format!("{}{}{}/{}", *BLUE, *STYLE_BOLD, display_name, *ALL_RESET)
} 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 linked_hash_map::LinkedHashMap;
use super::FileInArchive;
use crate::{utils::NoQuotePathFmt, warning};
#[derive(Debug, Default)]
pub struct Tree {
file: Option<FileInArchive>,
children: LinkedHashMap<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 is_dir = match self.file {
Some(FileInArchive { is_dir, .. }) => is_dir,
None => true,
};
super::print_entry(out, <Vec<u8> as ByteVec>::from_os_str_lossy(name).as_bstr(), is_dir);
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 = "├── ";
}
}