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