Merge pull request #253 from Crypto-Spartan/update-dependencies
[ouch.git] / src / list.rs
blobc8bd8ab1f355de1f4a2c5c28030bbf33abdb4faf
1 //! Implementation of the 'list' command, print list of files in an archive
3 use std::path::{Path, PathBuf};
5 use indicatif::{ProgressBar, ProgressStyle};
7 use self::tree::Tree;
9 /// Options controlling how archive contents should be listed
10 #[derive(Debug, Clone, Copy)]
11 pub struct ListOptions {
12     /// Whether to show a tree view
13     pub tree: bool,
16 /// Represents a single file in an archive, used in `list::list_files()`
17 #[derive(Debug, Clone)]
18 pub struct FileInArchive {
19     /// The file path
20     pub path: PathBuf,
22     /// Whether this file is a directory
23     pub is_dir: bool,
26 /// Actually print the files
27 /// Returns an Error, if one of the files can't be read
28 pub fn list_files(
29     archive: &Path,
30     files: impl IntoIterator<Item = crate::Result<FileInArchive>>,
31     list_options: ListOptions,
32 ) -> crate::Result<()> {
33     println!("Archive: {}", archive.display());
35     if list_options.tree {
36         let pb = if !crate::cli::ACCESSIBLE.get().unwrap() {
37             let template = "{wide_msg} [{elapsed_precise}] {spinner:.green}";
38             let pb = ProgressBar::new_spinner();
39             pb.set_style(ProgressStyle::default_bar().template(template));
40             Some(pb)
41         } else {
42             None
43         };
45         let tree: Tree = files
46             .into_iter()
47             .map(|file| {
48                 let file = file?;
49                 if !crate::cli::ACCESSIBLE.get().unwrap() {
50                     pb.as_ref().expect("exists").set_message(format!("Processing: {}", file.path.display()));
51                 }
52                 Ok(file)
53             })
54             .collect::<crate::Result<Tree>>()?;
55         drop(pb);
56         tree.print();
57     } else {
58         for file in files {
59             let FileInArchive { path, is_dir } = file?;
60             print_entry(path.display(), is_dir);
61         }
62     }
63     Ok(())
66 /// Print an entry and highlight directories, either by coloring them
67 /// if that's supported or by adding a trailing /
68 fn print_entry(name: impl std::fmt::Display, is_dir: bool) {
69     use crate::utils::colors::*;
71     if is_dir {
72         // if colors are deactivated, print final / to mark directories
73         if BLUE.is_empty() {
74             println!("{}/", name);
75         // if in ACCESSIBLE mode, use colors but print final / in case colors
76         // aren't read out aloud with a screen reader or aren't printed on a
77         // braille reader
78         } else if *crate::cli::ACCESSIBLE.get().unwrap() {
79             println!("{}{}{}/{}", *BLUE, *STYLE_BOLD, name, *ALL_RESET);
80         } else {
81             println!("{}{}{}{}", *BLUE, *STYLE_BOLD, name, *ALL_RESET);
82         }
83     } else {
84         // not a dir -> just print the file name
85         println!("{}", name);
86     }
89 /// Since archives store files as a list of entries -> without direct
90 /// directory structure (the directories are however part of the name),
91 /// we have to construct the tree structure ourselves to be able to
92 /// display them as a tree
93 mod tree {
94     use std::{ffi::OsString, iter::FromIterator, path};
96     use linked_hash_map::LinkedHashMap;
98     use super::FileInArchive;
100     /// Directory tree
101     #[derive(Debug, Default)]
102     pub struct Tree {
103         file: Option<FileInArchive>,
104         children: LinkedHashMap<OsString, Tree>,
105     }
106     impl Tree {
107         /// Insert a file into the tree
108         pub fn insert(&mut self, file: FileInArchive) {
109             self.insert_(file.clone(), file.path.iter());
110         }
111         /// Insert file by traversing the tree recursively
112         fn insert_(&mut self, file: FileInArchive, mut path: path::Iter) {
113             // Are there more components in the path? -> traverse tree further
114             if let Some(part) = path.next() {
115                 // Either insert into an existing child node or create a new one
116                 if let Some(t) = self.children.get_mut(part) {
117                     t.insert_(file, path)
118                 } else {
119                     let mut child = Tree::default();
120                     child.insert_(file, path);
121                     self.children.insert(part.to_os_string(), child);
122                 }
123             } else {
124                 // `path` was empty -> we reached our destination and can insert
125                 // `file`, assuming there is no file already there (which meant
126                 // there were 2 files with the same name in the same directory
127                 // which should be impossible in any sane file system)
128                 match &self.file {
129                     None => self.file = Some(file),
130                     Some(file) => {
131                         eprintln!(
132                             "[warning] multiple files with the same name in a single directory ({})",
133                             file.path.display()
134                         )
135                     }
136                 }
137             }
138         }
140         /// Print the file tree using Unicode line characters
141         pub fn print(&self) {
142             for (i, (name, subtree)) in self.children.iter().enumerate() {
143                 subtree.print_(name, String::new(), i == self.children.len() - 1);
144             }
145         }
146         /// Print the tree by traversing it recursively
147         fn print_(&self, name: &OsString, mut prefix: String, last: bool) {
148             // Convert `name` to valid unicode
149             let name = name.to_string_lossy();
151             // If there are no further elements in the parent directory, add
152             // "└── " to the prefix, otherwise add "├── "
153             let final_part = match last {
154                 true => draw::FINAL_LAST,
155                 false => draw::FINAL_BRANCH,
156             };
158             print!("{}{}", prefix, final_part);
159             let is_dir = match self.file {
160                 Some(FileInArchive { is_dir, .. }) => is_dir,
161                 None => true,
162             };
163             super::print_entry(name, is_dir);
165             // Construct prefix for children, adding either a line if this isn't
166             // the last entry in the parent dir or empty space if it is.
167             prefix.push_str(match last {
168                 true => draw::PREFIX_EMPTY,
169                 false => draw::PREFIX_LINE,
170             });
171             // Recursively print all children
172             for (i, (name, subtree)) in self.children.iter().enumerate() {
173                 subtree.print_(name, prefix.clone(), i == self.children.len() - 1);
174             }
175         }
176     }
178     impl FromIterator<FileInArchive> for Tree {
179         fn from_iter<I: IntoIterator<Item = FileInArchive>>(iter: I) -> Self {
180             let mut tree = Self::default();
181             for file in iter {
182                 tree.insert(file);
183             }
184             tree
185         }
186     }
188     /// Constants containing the visual parts of which the displayed tree
189     /// is constructed.
190     ///
191     /// They fall into 2 categories: the `PREFIX_*` parts form the first
192     /// `depth - 1` parts while the `FINAL_*` parts form the last part,
193     /// right before the entry itself
194     ///
195     /// `PREFIX_EMPTY`: the corresponding dir is the last entry in its parent dir
196     /// `PREFIX_LINE`: there are other entries after the corresponding dir
197     /// `FINAL_LAST`: this entry is the last entry in its parent dir
198     /// `FINAL_BRANCH`: there are other entries after this entry
199     mod draw {
200         /// the corresponding dir is the last entry in its parent dir
201         pub const PREFIX_EMPTY: &str = "   ";
202         /// there are other entries after the corresponding dir
203         pub const PREFIX_LINE: &str = "│  ";
204         /// this entry is the last entry in its parent dir
205         pub const FINAL_LAST: &str = "└── ";
206         /// there are other entries after this entry
207         pub const FINAL_BRANCH: &str = "├── ";
208     }