CI: tweak: reference reusable workflow locally
[ouch.git] / src / list.rs
blobc065925b7a6cebeb753197a03d039cc03fbac274
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         path,
82     };
84     use bstr::{ByteSlice, ByteVec};
85     use linked_hash_map::LinkedHashMap;
87     use super::FileInArchive;
88     use crate::utils::{logger::warning, EscapedPathDisplay};
90     /// Directory tree
91     #[derive(Debug, Default)]
92     pub struct Tree {
93         file: Option<FileInArchive>,
94         children: LinkedHashMap<OsString, Tree>,
95     }
97     impl Tree {
98         /// Insert a file into the tree
99         pub fn insert(&mut self, file: FileInArchive) {
100             self.insert_(file.clone(), file.path.iter());
101         }
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)
109                 } else {
110                     let mut child = Tree::default();
111                     child.insert_(file, path);
112                     self.children.insert(part.to_os_string(), child);
113                 }
114             } else {
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)
119                 match &self.file {
120                     None => self.file = Some(file),
121                     Some(file) => {
122                         warning(format!(
123                             "multiple files with the same name in a single directory ({})",
124                             EscapedPathDisplay::new(&file.path),
125                         ));
126                     }
127                 }
128             }
129         }
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);
135             }
136         }
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,
144             };
146             print!("{prefix}{final_part}");
147             let is_dir = match self.file {
148                 Some(FileInArchive { is_dir, .. }) => is_dir,
149                 None => true,
150             };
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,
159             });
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);
163             }
164         }
165     }
167     impl FromIterator<FileInArchive> for Tree {
168         fn from_iter<I: IntoIterator<Item = FileInArchive>>(iter: I) -> Self {
169             let mut tree = Self::default();
170             for file in iter {
171                 tree.insert(file);
172             }
173             tree
174         }
175     }
177     /// Constants containing the visual parts of which the displayed tree
178     /// is constructed.
179     ///
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
183     ///
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
188     mod draw {
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 = "├── ";
197     }