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