use {
cursive::{style::*, utils::span::*, view::*, views::*, *},
cursive_tree::*,
std::{env::*, fs::*, io, path::*, sync::*, time::*},
};
fn main() -> Result<(), io::Error> {
let mut cursive = default();
let mut tree = FileBackend::tree_view(current_dir()?.into());
tree.model.populate(Some(1))?;
cursive.add_fullscreen_layer(
LinearLayout::horizontal()
.child(Panel::new(tree.with_name("tree").scrollable()))
.child(Panel::new(TextView::empty().with_name("details").scrollable())),
);
cursive.add_global_callback('q', |cursive| cursive.quit());
cursive.run();
Ok(())
}
struct FileBackend;
impl TreeBackend for FileBackend {
type Context = Arc<PathBuf>;
type Error = io::Error;
type ID = PathBuf;
type Data = Metadata;
fn roots(base_directory: Self::Context) -> Result<NodeList<Self>, Self::Error> {
node_list(0, base_directory.as_ref())
}
fn populate(node: &mut Node<Self>, _base_directory: Self::Context) -> Result<(), Self::Error> {
node.children = Some(node_list(node.depth + 1, &node.id)?);
Ok(())
}
fn data(node: &mut Node<Self>, _base_directory: Self::Context) -> Result<Option<(Self::Data, bool)>, Self::Error> {
Ok(Some((node.id.metadata()?, true)))
}
fn handle_selection_changed(cursive: &mut Cursive, base_directory: Self::Context) {
let content = match cursive.call_on_name("tree", |tree: &mut TreeView<Self>| {
let base_directory = tree.model.context.clone();
Ok(match tree.selected_node_mut() {
Some(node) => match node.data(base_directory)? {
Some(metadata) => Some(format_metadata(&metadata)?),
None => None,
},
None => None,
})
}) {
Some(Ok(Some(text))) => text,
Some(Err(error)) => return Self::handle_error(cursive, base_directory, error),
_ => "".into(),
};
cursive.call_on_name("details", |details: &mut TextView| details.set_content(content));
}
fn handle_error(cursive: &mut Cursive, _base_directory: Self::Context, error: Self::Error) {
cursive.add_layer(
Dialog::around(TextView::new(error.to_string()))
.title("I/O Error")
.button("OK", |cursive| _ = cursive.pop_layer()),
);
}
}
fn node_list(depth: usize, directory: &PathBuf) -> Result<NodeList<FileBackend>, io::Error> {
let mut list = NodeList::default();
for entry in read_dir(directory)? {
let entry = entry?;
let kind = if entry.file_type()?.is_dir() { NodeKind::Branch } else { NodeKind::Leaf };
let path = entry.path();
let file_name = entry.file_name();
let file_name = file_name.to_string_lossy();
if kind.is_branch() {
let mut file_name_ = SpannedString::default();
file_name_.append_styled(file_name, Style::primary().combine(Effect::Bold));
list.add(depth, kind, path, file_name_);
} else {
list.add(depth, kind, path, file_name);
}
}
list.0.sort_by(|a: &Node<FileBackend>, b: &Node<FileBackend>| a.id.cmp(&b.id));
Ok(list)
}
fn format_metadata(metadata: &Metadata) -> Result<SpannedString<Style>, io::Error> {
let mut text = Default::default();
append_field(&mut text, "Created", &format_system_time(metadata.created()?));
text.append('\n');
append_field(&mut text, "Modified", &format_system_time(metadata.modified()?));
text.append('\n');
append_field(&mut text, "Accessed", &format_system_time(metadata.accessed()?));
text.append('\n');
text.append('\n');
append_field(&mut text, "Read-only", &metadata.permissions().readonly().to_string());
Ok(text.canonical())
}
fn append_field(text: &mut SpannedString<Style>, field: &str, value: &str) {
text.append_styled(field, Style::primary().combine(Effect::Bold));
text.append(": ");
text.append(value);
}
fn format_system_time(system_time: SystemTime) -> String {
format!("{} minutes ago", system_time.elapsed().unwrap_or_default().as_secs() / 60)
}