1use anyhow::{Context, Result};
4use hexz_core::format::header::Header;
5use hexz_core::format::index::MasterIndex;
6use indicatif::HumanBytes;
7use std::collections::{HashMap, HashSet};
8use std::fs::File;
9use std::path::{Path, PathBuf};
10
11use colored::Colorize;
12use crate::ui::color::{Palette, palette};
13
14struct ArchiveInfo {
15 path: PathBuf,
16 file_size: u64,
17 parent: Option<String>,
19 data_blocks: usize,
21}
22
23fn read_archive_info(path: &Path) -> Result<ArchiveInfo> {
24 use std::io::{Read, Seek, SeekFrom};
25
26 use hexz_core::format::index::IndexPage;
27
28 let mut f = File::open(path)?;
29 let file_size = f.metadata()?.len();
30 let header = Header::read_from(&mut f)?;
31 let master = MasterIndex::read_from(&mut f, header.index_offset)?;
32
33 let parent = header.parent_paths.into_iter().next();
34
35 let mut data_blocks = 0usize;
36 for page_meta in &master.main_pages {
37 let _ = f.seek(SeekFrom::Start(page_meta.offset))?;
38 let mut buf = vec![0u8; page_meta.length as usize];
39 f.read_exact(&mut buf)?;
40 let page: IndexPage = bincode::deserialize(&buf)?;
41 for block in page.blocks {
42 if !block.is_sparse() && !block.is_parent_ref() {
43 data_blocks += 1;
44 }
45 }
46 }
47
48 Ok(ArchiveInfo {
49 path: path.to_path_buf(),
50 file_size,
51 parent,
52 data_blocks,
53 })
54}
55
56fn print_tree(
57 idx: usize,
58 entries: &[ArchiveInfo],
59 children: &HashMap<usize, Vec<usize>>,
60 external_parent: &[Option<&str>],
61 prefix: &str,
62 is_last: bool,
63 p: &'static Palette,
64) {
65 let a = &entries[idx];
66 let connector = if is_last { "└──" } else { "├──" };
67 let name = a.path.file_name().unwrap_or_default().to_string_lossy();
68
69 let (ann_text, ann_color) = if let Some(ext) = external_parent[idx] {
70 let parent_name = Path::new(ext)
71 .file_name()
72 .unwrap_or_default()
73 .to_string_lossy()
74 .into_owned();
75 (format!("← {parent_name} (external)"), p.gray)
76 } else if a.parent.is_none() {
77 ("standalone".to_string(), p.gray)
78 } else {
79 (format!("+{} new blocks", a.data_blocks), p.dim)
80 };
81
82 let name_padded = format!("{name:<32}");
83 let size_str = format!("{:>10}", HumanBytes(a.file_size));
84
85 println!(
86 " {}{}{}{} {}{}{} {}{}{} {}{}{}",
87 prefix,
88 p.gray,
89 connector,
90 p.reset,
91 p.bold,
92 name_padded,
93 p.reset,
94 p.green,
95 size_str,
96 p.reset,
97 ann_color,
98 ann_text,
99 p.reset,
100 );
101
102 let mut kids = children.get(&idx).cloned().unwrap_or_default();
103 kids.sort_by_key(|&i| &entries[i].path);
104
105 let segment = if is_last {
106 " ".to_string()
107 } else {
108 format!("{}│{} ", p.gray, p.reset)
109 };
110 let child_prefix = format!("{prefix}{segment}");
111
112 for (j, &child) in kids.iter().enumerate() {
113 let last = j == kids.len() - 1;
114 print_tree(
115 child,
116 entries,
117 children,
118 external_parent,
119 &child_prefix,
120 last,
121 p,
122 );
123 }
124}
125
126pub fn run(dir: &Path) -> Result<()> {
128 let entries: Vec<ArchiveInfo> = std::fs::read_dir(dir)
129 .with_context(|| format!("Cannot read directory: {}", dir.display()))?
130 .filter_map(std::result::Result::ok)
131 .filter(|e| e.path().extension().is_some_and(|ext| ext == "hxz"))
132 .map(|e| {
133 let p = e.path();
134 read_archive_info(&p).with_context(|| format!("Failed to read {}", p.display()))
135 })
136 .collect::<Result<Vec<_>>>()?;
137
138 if entries.is_empty() {
139 println!("No .hxz archives found in {}", dir.display());
140 return Ok(());
141 }
142
143 let name_to_idx: HashMap<String, usize> = entries
144 .iter()
145 .enumerate()
146 .map(|(i, a)| {
147 let name = a
148 .path
149 .file_name()
150 .unwrap_or_default()
151 .to_string_lossy()
152 .into_owned();
153 (name, i)
154 })
155 .collect();
156
157 let parent_idx: Vec<Option<usize>> = entries
158 .iter()
159 .map(|a| {
160 a.parent.as_deref().and_then(|p| {
161 let parent_name = Path::new(p)
162 .file_name()
163 .unwrap_or_default()
164 .to_string_lossy()
165 .into_owned();
166 name_to_idx.get(&parent_name).copied()
167 })
168 })
169 .collect();
170
171 let external_parent: Vec<Option<&str>> = entries
172 .iter()
173 .zip(&parent_idx)
174 .map(|(a, resolved)| {
175 if resolved.is_none() {
176 a.parent.as_deref()
177 } else {
178 None
179 }
180 })
181 .collect();
182
183 let mut children: HashMap<usize, Vec<usize>> = HashMap::new();
184 let mut has_parent: HashSet<usize> = HashSet::new();
185 for (i, p) in parent_idx.iter().enumerate() {
186 if let Some(pi) = p {
187 children.entry(*pi).or_default().push(i);
188 let _ = has_parent.insert(i);
189 }
190 }
191
192 let mut roots: Vec<usize> = (0..entries.len())
193 .filter(|i| !has_parent.contains(i))
194 .collect();
195 roots.sort_by_key(|&i| &entries[i].path);
196
197 let p = palette();
198 let total_size: u64 = entries.iter().map(|a| a.file_size).sum();
199
200 let dir_str = dir.to_string_lossy();
201 let dir_base = dir_str.trim_end_matches('/');
202 println!("{} {}/", "╭".dimmed(), dir_base.cyan());
203
204 for (i, &root) in roots.iter().enumerate() {
205 let last = i == roots.len() - 1;
206 print_tree(root, &entries, &children, &external_parent, "│ ".dimmed().to_string().as_str(), last, p);
207 }
208
209 let archive_count = format!("{} archive{}", entries.len(), if entries.len() == 1 { "" } else { "s" });
210 println!(
211 "{} {} {} on disk",
212 "╰".dimmed(),
213 archive_count.bright_black(),
214 HumanBytes(total_size).to_string().green(),
215 );
216
217 Ok(())
218}