rusty_roots/
directory.rs

1use std::cmp::max;
2use std::collections::HashMap;
3use std::fs;
4use std::io::Error;
5use std::io::ErrorKind;
6use std::path::{Path, PathBuf};
7
8use globset::GlobSet;
9
10use crate::ignore::load_ignore_patterns;
11use crate::text_fmt::{color_branch, color_path};
12
13type DirectoryBody = HashMap<PathBuf, Vec<PathBuf>>;
14
15const WALL: &str = "│   ";
16const MIDDLE_CHILD: &str = "├── ";
17const LAST_CHILD: &str = "└── ";
18const SPACE: &str = "    ";
19
20#[derive(Debug)]
21pub struct Directory {
22    root: PathBuf,
23    body: DirectoryBody,
24    max_depth: u8,
25    ignore_patterns: GlobSet,
26}
27
28impl Directory {
29    pub fn new(root: &Path, ignore: bool) -> Result<Self, Error> {
30        let mut res = Self {
31            root: root.to_path_buf(),
32            body: HashMap::new(),
33            max_depth: 0,
34            ignore_patterns: if ignore {
35                load_ignore_patterns(root.to_path_buf())?
36            } else {
37                GlobSet::empty()
38            },
39        };
40        res.walk_dir(root, 1)?;
41        Ok(res)
42    }
43
44    pub fn new_with_empty_body(root: &Path, ignore: bool) -> Result<Self, Error> {
45        let res = Self {
46            root: root.to_path_buf(),
47            body: HashMap::new(),
48            max_depth: 0,
49            ignore_patterns: if ignore {
50                load_ignore_patterns(root.to_path_buf())?
51            } else {
52                GlobSet::empty()
53            },
54        };
55        Ok(res)
56    }
57
58    /// Populates the Directory object by recursively walking filepaths
59    fn walk_dir(&mut self, from: &Path, depth: u8) -> Result<(), Error> {
60        if !from.to_path_buf().is_dir() {
61            let err_msg = format!("{} is not a directory", from.display());
62            return Err(Error::new(ErrorKind::InvalidInput, err_msg));
63        }
64        self.max_depth = max(self.max_depth, depth);
65
66        for entry in fs::read_dir(from)? {
67            let entry = entry?;
68            let path = entry.path();
69            // add info to directory body
70            // let trimmed_path = path.to_str().unwrap().trim_start_matches("./");
71            if !self.ignore_patterns.is_match(&path) {
72                match self.body.get_mut(from) {
73                    Some(v) => v.push(path.clone()),
74                    None => {
75                        self.body.insert(from.to_path_buf(), vec![path.clone()]);
76                    }
77                }
78                // dfs if path is directory
79                if path.is_dir() {
80                    self.walk_dir(&path, depth + 1)?;
81                }
82            }
83        }
84        return Ok(());
85    }
86
87    /// Wrapper function to print Directory body's structure.
88    pub fn print_body(&self) -> Result<(), Error> {
89        self.print_dir(None, &mut Vec::new(), None)?;
90        println!();
91        Ok(())
92    }
93
94    /// Takes a dfs approach to printing the directory structure.
95    /// For each iteration we print the current path, then alphabetically print children.
96    fn print_dir(
97        &self,
98        cur: Option<&Path>,
99        wall_list: &mut Vec<bool>,
100        pos: Option<&str>,
101    ) -> Result<(), Error> {
102        let cur = cur.unwrap_or(&self.root);
103        let pos = pos.unwrap_or(LAST_CHILD);
104
105        // print line for given entry
106        println!();
107        for (i, w) in wall_list.iter().enumerate() {
108            if *w {
109                print!("{}", color_branch(WALL, i as u8, self.max_depth));
110            } else {
111                print!("{}", color_branch(SPACE, i as u8, self.max_depth))
112            }
113        }
114        print!(
115            "{}",
116            color_branch(pos, wall_list.len() as u8, self.max_depth)
117        );
118        print!("{}", color_path(cur));
119
120        // update wall list for next call (add to stack)
121        wall_list.push(pos != LAST_CHILD);
122
123        // dfs
124        let mut children = self.body.get(cur).unwrap_or(&Vec::new()).clone();
125        children.sort_by(|a, b| a.to_str().unwrap().cmp(b.to_str().unwrap()));
126        for (i, child) in children.iter().enumerate() {
127            let child_pos = if i == children.len() - 1 {
128                LAST_CHILD
129            } else {
130                MIDDLE_CHILD
131            };
132            self.print_dir(Some(&child), wall_list, Some(child_pos))?;
133        }
134
135        // update wall list for next call (rm from stack)
136        wall_list.pop();
137
138        Ok(())
139    }
140
141    /// For printing from an empty directory state when concerned about speed.
142    /// We output the tree as we traverse our directory tree.
143    pub fn fast_print_body(&self) -> Result<(), Error> {
144        self.fast_print_dir(None, &mut Vec::new(), None)?;
145        println!();
146        Ok(())
147    }
148
149    /// To be called from fast_print_body as a helper.
150    fn fast_print_dir(
151        &self,
152        cur: Option<&Path>,
153        wall_list: &mut Vec<bool>,
154        pos: Option<&str>,
155    ) -> Result<(), Error> {
156        let cur = cur.unwrap_or(&self.root);
157        let pos = pos.unwrap_or(LAST_CHILD);
158
159        // print line for given entry
160        println!();
161        for w in wall_list.iter() {
162            if *w {
163                print!("{}", WALL);
164            } else {
165                print!("{}", SPACE)
166            }
167        }
168        print!("{}", pos);
169        print!("{}", color_path(cur));
170
171        // update wall list for next call (add to stack)
172        wall_list.push(pos != LAST_CHILD);
173
174        // return if leaf
175        if !cur.to_path_buf().is_dir() {
176            wall_list.pop();
177            return Ok(());
178        }
179
180        // dfs
181        let mut children = Vec::new();
182        for entry in fs::read_dir(cur)? {
183            let entry = entry?;
184            let path = entry.path();
185            // add info to children based on ignore patterns
186            if !self.ignore_patterns.is_match(&path) {
187                children.push(path);
188            }
189        }
190        children.sort_by(|a, b| a.to_str().unwrap().cmp(b.to_str().unwrap()));
191        for (i, child) in children.iter().enumerate() {
192            let child_pos = if i == children.len() - 1 {
193                LAST_CHILD
194            } else {
195                MIDDLE_CHILD
196            };
197            self.fast_print_dir(Some(&child), wall_list, Some(child_pos))?;
198        }
199
200        // update wall list for next call (rm from stack)
201        wall_list.pop();
202
203        Ok(())
204    }
205}