file_browser/
file_browser.rs1use {
2 cursive::{style::*, utils::span::*, view::*, views::*, *},
3 cursive_tree::*,
4 std::{env::*, fs::*, io, path::*, sync::*, time::*},
5};
6
7fn main() -> Result<(), io::Error> {
17 let mut cursive = default();
18
19 let mut tree = FileBackend::tree_view(current_dir()?.into());
21
22 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 type Context = Arc<PathBuf>;
45
46 type Error = io::Error;
48
49 type ID = PathBuf;
51
52 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 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 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 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 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}