1 //! Some implementation helpers related to the 'list' command.
9 use crate::{accessible::is_running_in_accessible_mode, utils::EscapedPathDisplay};
11 /// Options controlling how archive contents should be listed
12 #[derive(Debug, Clone, Copy)]
13 pub struct ListOptions {
14 /// Whether to show a tree view
18 /// Represents a single file in an archive, used in `list::list_files()`
19 #[derive(Debug, Clone)]
20 pub struct FileInArchive {
24 /// Whether this file is a directory
28 /// Actually print the files
29 /// Returns an Error, if one of the files can't be read
32 files: impl IntoIterator<Item = crate::Result<FileInArchive>>,
33 list_options: ListOptions,
34 ) -> crate::Result<()> {
35 let out = &mut stdout().lock();
36 let _ = writeln!(out, "Archive: {}", EscapedPathDisplay::new(archive));
38 if list_options.tree {
39 let tree = files.into_iter().collect::<crate::Result<Tree>>()?;
43 let FileInArchive { path, is_dir } = file?;
44 print_entry(out, EscapedPathDisplay::new(&path), is_dir);
50 /// Print an entry and highlight directories, either by coloring them
51 /// if that's supported or by adding a trailing /
52 fn print_entry(out: &mut impl Write, name: impl std::fmt::Display, is_dir: bool) {
53 use crate::utils::colors::*;
56 // if colors are deactivated, print final / to mark directories
58 let _ = writeln!(out, "{name}/");
59 // if in ACCESSIBLE mode, use colors but print final / in case colors
60 // aren't read out aloud with a screen reader or aren't printed on a
62 } else if is_running_in_accessible_mode() {
63 let _ = writeln!(out, "{}{}{}/{}", *BLUE, *STYLE_BOLD, name, *ALL_RESET);
65 let _ = writeln!(out, "{}{}{}{}", *BLUE, *STYLE_BOLD, name, *ALL_RESET);
68 // not a dir -> just print the file name
69 let _ = writeln!(out, "{name}");
73 /// Since archives store files as a list of entries -> without direct
74 /// directory structure (the directories are however part of the name),
75 /// we have to construct the tree structure ourselves to be able to
76 /// display them as a tree
79 ffi::{OsStr, OsString},
84 use bstr::{ByteSlice, ByteVec};
85 use linked_hash_map::LinkedHashMap;
87 use super::FileInArchive;
88 use crate::utils::{logger::warning, EscapedPathDisplay};
91 #[derive(Debug, Default)]
93 file: Option<FileInArchive>,
94 children: LinkedHashMap<OsString, Tree>,
98 /// Insert a file into the tree
99 pub fn insert(&mut self, file: FileInArchive) {
100 self.insert_(file.clone(), file.path.iter());
102 /// Insert file by traversing the tree recursively
103 fn insert_(&mut self, file: FileInArchive, mut path: path::Iter) {
104 // Are there more components in the path? -> traverse tree further
105 if let Some(part) = path.next() {
106 // Either insert into an existing child node or create a new one
107 if let Some(t) = self.children.get_mut(part) {
108 t.insert_(file, path)
110 let mut child = Tree::default();
111 child.insert_(file, path);
112 self.children.insert(part.to_os_string(), child);
115 // `path` was empty -> we reached our destination and can insert
116 // `file`, assuming there is no file already there (which meant
117 // there were 2 files with the same name in the same directory
118 // which should be impossible in any sane file system)
120 None => self.file = Some(file),
123 "multiple files with the same name in a single directory ({})",
124 EscapedPathDisplay::new(&file.path),
131 /// Print the file tree using Unicode line characters
132 pub fn print(&self, out: &mut impl Write) {
133 for (i, (name, subtree)) in self.children.iter().enumerate() {
134 subtree.print_(out, name, "", i == self.children.len() - 1);
137 /// Print the tree by traversing it recursively
138 fn print_(&self, out: &mut impl Write, name: &OsStr, prefix: &str, last: bool) {
139 // If there are no further elements in the parent directory, add
140 // "└── " to the prefix, otherwise add "├── "
141 let final_part = match last {
142 true => draw::FINAL_LAST,
143 false => draw::FINAL_BRANCH,
146 print!("{prefix}{final_part}");
147 let is_dir = match self.file {
148 Some(FileInArchive { is_dir, .. }) => is_dir,
151 super::print_entry(out, <Vec<u8> as ByteVec>::from_os_str_lossy(name).as_bstr(), is_dir);
153 // Construct prefix for children, adding either a line if this isn't
154 // the last entry in the parent dir or empty space if it is.
155 let mut prefix = prefix.to_owned();
156 prefix.push_str(match last {
157 true => draw::PREFIX_EMPTY,
158 false => draw::PREFIX_LINE,
160 // Recursively print all children
161 for (i, (name, subtree)) in self.children.iter().enumerate() {
162 subtree.print_(out, name, &prefix, i == self.children.len() - 1);
167 impl FromIterator<FileInArchive> for Tree {
168 fn from_iter<I: IntoIterator<Item = FileInArchive>>(iter: I) -> Self {
169 let mut tree = Self::default();
177 /// Constants containing the visual parts of which the displayed tree
180 /// They fall into 2 categories: the `PREFIX_*` parts form the first
181 /// `depth - 1` parts while the `FINAL_*` parts form the last part,
182 /// right before the entry itself
184 /// `PREFIX_EMPTY`: the corresponding dir is the last entry in its parent dir
185 /// `PREFIX_LINE`: there are other entries after the corresponding dir
186 /// `FINAL_LAST`: this entry is the last entry in its parent dir
187 /// `FINAL_BRANCH`: there are other entries after this entry
189 /// the corresponding dir is the last entry in its parent dir
190 pub const PREFIX_EMPTY: &str = " ";
191 /// there are other entries after the corresponding dir
192 pub const PREFIX_LINE: &str = "│ ";
193 /// this entry is the last entry in its parent dir
194 pub const FINAL_LAST: &str = "└── ";
195 /// there are other entries after this entry
196 pub const FINAL_BRANCH: &str = "├── ";