add tests for CLI usage
[ouch.git] / src / list.rs
blob4d4a9452485f4e60aad204a7c7b3387a363bb847
1 //! Some implementation helpers related to the 'list' command.
3 use std::{
4     io::{stdout, Write},
5     path::{Path, PathBuf},
6 };
8 use self::tree::Tree;
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
15     pub tree: bool,
18 /// Represents a single file in an archive, used in `list::list_files()`
19 #[derive(Debug, Clone)]
20 pub struct FileInArchive {
21     /// The file path
22     pub path: PathBuf,
24     /// Whether this file is a directory
25     pub is_dir: bool,
28 /// Actually print the files
29 /// Returns an Error, if one of the files can't be read
30 pub fn list_files(
31     archive: &Path,
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>>()?;
40         tree.print(out);
41     } else {
42         for file in files {
43             let FileInArchive { path, is_dir } = file?;
44             print_entry(out, EscapedPathDisplay::new(&path), is_dir);
45         }
46     }
47     Ok(())
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::*;
55     if is_dir {
56         // if colors are deactivated, print final / to mark directories
57         if BLUE.is_empty() {
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
61         // braille reader
62         } else if is_running_in_accessible_mode() {
63             let _ = writeln!(out, "{}{}{}/{}", *BLUE, *STYLE_BOLD, name, *ALL_RESET);
64         } else {
65             let _ = writeln!(out, "{}{}{}{}", *BLUE, *STYLE_BOLD, name, *ALL_RESET);
66         }
67     } else {
68         // not a dir -> just print the file name
69         let _ = writeln!(out, "{name}");
70     }
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
77 mod tree {
78     use std::{
79         ffi::{OsStr, OsString},
80         io::Write,
81         iter::FromIterator,
82         path,
83     };
85     use bstr::{ByteSlice, ByteVec};
86     use linked_hash_map::LinkedHashMap;
88     use super::FileInArchive;
89     use crate::{utils::EscapedPathDisplay, warning};
91     /// Directory tree
92     #[derive(Debug, Default)]
93     pub struct Tree {
94         file: Option<FileInArchive>,
95         children: LinkedHashMap<OsString, Tree>,
96     }
98     impl Tree {
99         /// Insert a file into the tree
100         pub fn insert(&mut self, file: FileInArchive) {
101             self.insert_(file.clone(), file.path.iter());
102         }
103         /// Insert file by traversing the tree recursively
104         fn insert_(&mut self, file: FileInArchive, mut path: path::Iter) {
105             // Are there more components in the path? -> traverse tree further
106             if let Some(part) = path.next() {
107                 // Either insert into an existing child node or create a new one
108                 if let Some(t) = self.children.get_mut(part) {
109                     t.insert_(file, path)
110                 } else {
111                     let mut child = Tree::default();
112                     child.insert_(file, path);
113                     self.children.insert(part.to_os_string(), child);
114                 }
115             } else {
116                 // `path` was empty -> we reached our destination and can insert
117                 // `file`, assuming there is no file already there (which meant
118                 // there were 2 files with the same name in the same directory
119                 // which should be impossible in any sane file system)
120                 match &self.file {
121                     None => self.file = Some(file),
122                     Some(file) => {
123                         warning!(
124                             "multiple files with the same name in a single directory ({})",
125                             EscapedPathDisplay::new(&file.path),
126                         );
127                     }
128                 }
129             }
130         }
132         /// Print the file tree using Unicode line characters
133         pub fn print(&self, out: &mut impl Write) {
134             for (i, (name, subtree)) in self.children.iter().enumerate() {
135                 subtree.print_(out, name, "", i == self.children.len() - 1);
136             }
137         }
138         /// Print the tree by traversing it recursively
139         fn print_(&self, out: &mut impl Write, name: &OsStr, prefix: &str, last: bool) {
140             // If there are no further elements in the parent directory, add
141             // "└── " to the prefix, otherwise add "├── "
142             let final_part = match last {
143                 true => draw::FINAL_LAST,
144                 false => draw::FINAL_BRANCH,
145             };
147             print!("{prefix}{final_part}");
148             let is_dir = match self.file {
149                 Some(FileInArchive { is_dir, .. }) => is_dir,
150                 None => true,
151             };
152             super::print_entry(out, <Vec<u8> as ByteVec>::from_os_str_lossy(name).as_bstr(), is_dir);
154             // Construct prefix for children, adding either a line if this isn't
155             // the last entry in the parent dir or empty space if it is.
156             let mut prefix = prefix.to_owned();
157             prefix.push_str(match last {
158                 true => draw::PREFIX_EMPTY,
159                 false => draw::PREFIX_LINE,
160             });
161             // Recursively print all children
162             for (i, (name, subtree)) in self.children.iter().enumerate() {
163                 subtree.print_(out, name, &prefix, i == self.children.len() - 1);
164             }
165         }
166     }
168     impl FromIterator<FileInArchive> for Tree {
169         fn from_iter<I: IntoIterator<Item = FileInArchive>>(iter: I) -> Self {
170             let mut tree = Self::default();
171             for file in iter {
172                 tree.insert(file);
173             }
174             tree
175         }
176     }
178     /// Constants containing the visual parts of which the displayed tree
179     /// is constructed.
180     ///
181     /// They fall into 2 categories: the `PREFIX_*` parts form the first
182     /// `depth - 1` parts while the `FINAL_*` parts form the last part,
183     /// right before the entry itself
184     ///
185     /// `PREFIX_EMPTY`: the corresponding dir is the last entry in its parent dir
186     /// `PREFIX_LINE`: there are other entries after the corresponding dir
187     /// `FINAL_LAST`: this entry is the last entry in its parent dir
188     /// `FINAL_BRANCH`: there are other entries after this entry
189     mod draw {
190         /// the corresponding dir is the last entry in its parent dir
191         pub const PREFIX_EMPTY: &str = "   ";
192         /// there are other entries after the corresponding dir
193         pub const PREFIX_LINE: &str = "│  ";
194         /// this entry is the last entry in its parent dir
195         pub const FINAL_LAST: &str = "└── ";
196         /// there are other entries after this entry
197         pub const FINAL_BRANCH: &str = "├── ";
198     }