Skip to main content

file_browser/
file_browser.rs

1use {
2    cursive::{style::*, utils::span::*, view::*, views::*, *},
3    cursive_tree::*,
4    std::{env::*, fs::*, io, path::*, sync::*, time::*},
5};
6
7// In this example we'll be implementing our own backend to populate nodes on demand
8
9// It's a simple file browser for the current directory
10// The left pane is the tree
11// The right pane will show the selected node's metadata
12
13fn main() {
14    let mut cursive = Cursive::default();
15
16    let current_dir = current_dir().unwrap();
17    let mut tree = FileBackend::tree_view(current_dir.into());
18
19    // Populate the first level (just the roots)
20    tree.model.populate(Some(1)).unwrap();
21
22    let mut browser = LinearLayout::horizontal();
23    browser.add_child(Panel::new(tree.with_name("tree").scrollable()));
24    browser.add_child(Panel::new(TextView::new("").with_name("details").scrollable()));
25
26    cursive.add_fullscreen_layer(browser);
27    cursive.add_global_callback('q', |cursive| cursive.quit());
28
29    cursive.run();
30}
31
32struct FileBackend;
33
34impl TreeBackend for FileBackend {
35    // Our context will be the base directory
36    // We use it for populating the roots
37    // It could be a PathBuf, but we're choosing Arc<PathBuf> in order to make it cheaper to clone
38    type Context = Arc<PathBuf>;
39
40    // This is what all our functions return
41    type Error = io::Error;
42
43    // The path of the node itself
44    type ID = PathBuf;
45
46    // We'll use this for caching metadata to show in the details view
47    type Data = Metadata;
48
49    fn roots(base_directory: Arc<PathBuf>) -> Result<NodeList<Self>, io::Error> {
50        node_list(0, base_directory.as_ref())
51    }
52
53    fn populate(node: &mut Node<Self>, _base_directory: Arc<PathBuf>) -> Result<(), io::Error> {
54        node.children = Some(node_list(node.depth + 1, &node.id)?);
55        Ok(())
56    }
57
58    fn data(node: &mut Node<Self>, _base_directory: Arc<PathBuf>) -> Result<Option<(Metadata, bool)>, io::Error> {
59        // We return true to cache the metadata
60        Ok(Some((node.id.metadata()?, true)))
61    }
62
63    fn handle_selection_changed(cursive: &mut Cursive) {
64        let content = match cursive.call_on_name("tree", |tree: &mut TreeView<Self>| {
65            // Although we're not using the context in data() but we still need to provide it
66            let base_directory = tree.model.context.clone();
67            Ok(match tree.selected_node_mut() {
68                Some(node) => match node.data(base_directory)? {
69                    Some(metadata) => Some(format_metadata(&metadata)?),
70                    None => None,
71                },
72                None => None,
73            })
74        }) {
75            Some(Ok(Some(text))) => text,
76            Some(Err(error)) => return Self::handle_error(cursive, error),
77            _ => "".into(),
78        };
79
80        cursive.call_on_name("details", |details: &mut TextView| {
81            details.set_content(content);
82        });
83    }
84
85    fn handle_error(cursive: &mut Cursive, error: io::Error) {
86        // We'll popup a dialog with the error message
87        cursive.add_layer(Dialog::around(TextView::new(error.to_string())).title("I/O Error").button(
88            "OK",
89            |cursive| {
90                cursive.pop_layer();
91            },
92        ));
93    }
94}
95
96fn node_list(depth: usize, directory: &PathBuf) -> Result<NodeList<FileBackend>, io::Error> {
97    let mut list = NodeList::default();
98
99    for entry in read_dir(directory)? {
100        let entry = entry?;
101
102        let kind = if entry.file_type()?.is_dir() { NodeKind::Branch } else { NodeKind::Leaf };
103
104        let file_name = entry.file_name();
105        let file_name = file_name.to_string_lossy();
106
107        // We'll use different styles for leaves and branches
108        let mut representation = Representation::default();
109        if kind.is_branch() {
110            representation.append_styled(file_name, Style::primary().combine(Effect::Bold));
111        } else {
112            representation.append(file_name);
113        }
114
115        list.add(depth, kind, entry.path(), representation);
116    }
117
118    Ok(list)
119}
120
121fn format_metadata(metadata: &Metadata) -> Result<Representation, io::Error> {
122    let mut text = Default::default();
123
124    append_field(&mut text, "Created", &format_system_time(metadata.created()?));
125    append_field(&mut text, "Modified", &format_system_time(metadata.modified()?));
126    append_field(&mut text, "Accessed", &format_system_time(metadata.accessed()?));
127    text.append('\n');
128    append_field(&mut text, "Read-only", &metadata.permissions().readonly().to_string());
129
130    Ok(text.canonical())
131}
132
133fn append_field(text: &mut SpannedString<Style>, field: &str, value: &str) {
134    text.append_styled(field, Style::primary().combine(Effect::Bold));
135    text.append(": ");
136    text.append(value);
137    text.append('\n');
138}
139
140fn format_system_time(system_time: SystemTime) -> String {
141    format!("{} minutes ago", system_time.elapsed().unwrap().as_secs() / 60)
142}