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