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(tree.with_name("tree").scrollable().full_screen());
24    browser.add_child(TextView::new("").with_name("details").scrollable().full_screen());
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
34// Our context will be the base directory for populating the roots
35// It could be a PathBuf, but we're choosing Arc<PathBuf> in order to make it cheaper to clone
36
37// Our error type is io::Error, as that what all our functions will return
38
39// Our node ID type is PathBuf, which will be the path of the node itself
40
41// For our custom data type we'll use Metadata
42
43impl TreeBackend<Arc<PathBuf>, io::Error, PathBuf, Metadata> for FileBackend {
44    fn roots(
45        base_directory: Arc<PathBuf>,
46    ) -> Result<NodeList<Self, Arc<PathBuf>, io::Error, PathBuf, Metadata>, io::Error> {
47        node_list(0, base_directory.as_ref())
48    }
49
50    fn populate(
51        node: &mut Node<Self, Arc<PathBuf>, io::Error, PathBuf, Metadata>,
52        _base_directory: Arc<PathBuf>,
53    ) -> Result<(), io::Error> {
54        node.children = Some(node_list(node.depth + 1, &node.id)?);
55        Ok(())
56    }
57
58    fn data(
59        node: &mut Node<Self, Arc<PathBuf>, io::Error, PathBuf, Metadata>,
60        _base_directory: Arc<PathBuf>,
61    ) -> Result<Option<(Metadata, bool)>, io::Error> {
62        // We return true to cache the metadata
63        Ok(Some((node.id.metadata()?, true)))
64    }
65
66    fn handle_selection_changed(cursive: &mut Cursive) {
67        let content = match cursive.call_on_name(
68            "tree",
69            |tree: &mut TreeView<Self, Arc<PathBuf>, io::Error, PathBuf, Metadata>| {
70                // Although we're not using the context in data() but we still need to provide it
71                let base_directory = tree.model.context.clone();
72                Ok(match tree.selected_node_mut() {
73                    Some(node) => match node.data(base_directory)? {
74                        Some(metadata) => Some(format_metadata(&metadata)?),
75                        None => None,
76                    },
77                    None => None,
78                })
79            },
80        ) {
81            Some(Ok(Some(text))) => text,
82            Some(Err(error)) => return Self::handle_error(cursive, error),
83            _ => "".into(),
84        };
85
86        cursive.call_on_name("details", |details: &mut TextView| {
87            details.set_content(content);
88        });
89    }
90
91    fn handle_error(cursive: &mut Cursive, error: io::Error) {
92        // We'll popup a dialog with the error message
93        cursive.add_layer(Dialog::around(TextView::new(error.to_string())).title("I/O Error").button(
94            "OK",
95            |cursive| {
96                cursive.pop_layer();
97            },
98        ));
99    }
100}
101
102fn node_list(
103    depth: usize,
104    directory: &PathBuf,
105) -> Result<NodeList<FileBackend, Arc<PathBuf>, io::Error, PathBuf, Metadata>, io::Error> {
106    let mut list = NodeList::default();
107
108    for entry in read_dir(directory)? {
109        let entry = entry?;
110
111        let kind = if entry.file_type()?.is_dir() { NodeKind::Branch } else { NodeKind::Leaf };
112
113        let file_name = entry.file_name();
114        let file_name = file_name.to_string_lossy();
115
116        // We'll use different styles for leaves and branches
117        let mut representation = Representation::default();
118        if kind.is_branch() {
119            representation.append_styled(file_name, Style::title_primary());
120        } else {
121            representation.append_styled(file_name, Style::title_secondary());
122        }
123
124        list.add(depth, kind, entry.path(), representation);
125    }
126
127    Ok(list)
128}
129
130fn format_metadata(metadata: &Metadata) -> Result<Representation, io::Error> {
131    let mut text = Default::default();
132
133    append_field(&mut text, "Created", &format_system_time(metadata.created()?));
134    append_field(&mut text, "Modified", &format_system_time(metadata.modified()?));
135    append_field(&mut text, "Accessed", &format_system_time(metadata.accessed()?));
136    text.append('\n');
137    append_field(&mut text, "Read-only", &metadata.permissions().readonly().to_string());
138
139    Ok(text.canonical())
140}
141
142fn append_field(text: &mut SpannedString<Style>, field: &str, value: &str) {
143    text.append_styled(field, Style::title_secondary());
144    text.append(": ");
145    text.append(value);
146    text.append('\n');
147}
148
149fn format_system_time(system_time: SystemTime) -> String {
150    format!("{} minutes ago", system_time.elapsed().unwrap().as_secs() / 60)
151}