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