cursive-tree 0.0.9

Tree view for the Cursive TUI library
Documentation
use {
    cursive::{style::*, utils::span::*, view::*, views::*, *},
    cursive_tree::*,
    std::{env::*, fs::*, io, path::*, sync::*, time::*},
};

// In this example we'll be implementing our own backend
// to demonstrate populating nodes on demand

// It's a simple file browser
// The left pane is the tree
// The right pane will show the selected node's metadata

// (For an improved split panel see: https://codeberg.org/tliron/cursive-split-panel)

fn main() -> Result<(), io::Error> {
    let mut cursive = default();

    // Our base directory (the backend context) will be the current directory
    let mut tree = FileBackend::tree_view(current_dir()?.into());

    // Populate the first level (just the roots)
    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 {
    // Our context will be the base directory
    // We use it for populating the roots
    // It could be a PathBuf, but we're choosing Arc<PathBuf> in order to make it cheaper to clone
    type Context = Arc<PathBuf>;

    // This is what all our functions return
    type Error = io::Error;

    // The path of the node itself
    type ID = PathBuf;

    // We'll use this for caching metadata to show in the details view
    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> {
        // We return true to cache the metadata
        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>| {
            // Note that although we're not using the context in data() we still need to provide it
            // (some implementations might need it)
            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) {
        // We'll popup a dialog with the error message
        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();

        // We'll use different label styles for leaves and branches
        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)
}