appcui 0.4.8

A feature-rich and cross-platform TUI/CUI framework for Rust, enabling modern terminal-based applications on Windows, Linux, and macOS. Includes built-in UI components like buttons, menus, list views, tree views, checkboxes, and more. Perfect for building fast and interactive CLI tools and text-based interfaces.
Documentation
use std::path::{Component, PathBuf};
use std::sync::{Mutex, OnceLock};

use super::Location;
use super::SelectFolderDialogFlags;
use crate::prelude::*;
use crate::ui::pathfinder::GenericPathFinder;
use crate::utils::fs::{Entry, Root};
use crate::utils::Navigator;

pub(super) enum FolderSelectionDialogResult {
    Path(PathBuf),
    Cancel,
}

#[derive(ListItem)]
struct FolderName {
    #[Column(name: "&Name", width: 200)]
    value: String,
}

pub(super) static FOLDER_LAST_PATH: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();

#[ModalWindow(events = ButtonEvents+WindowEvents+PathFinderEvents+TreeViewEvents<FolderName>, response: FolderSelectionDialogResult, internal: true)]
pub(super) struct FolderExplorer<T>
where
    T: Navigator<Entry, Root, PathBuf> + 'static,
{
    path_viewer: Handle<GenericPathFinder<T>>,
    tv: Handle<TreeView<FolderName>>,
    b_ok: Handle<Button>,
    b_cancel: Handle<Button>,
    nav: T,
    path: PathBuf,
    flags: SelectFolderDialogFlags,
}

impl<T> FolderExplorer<T>
where
    T: Navigator<Entry, Root, PathBuf> + 'static,
{
    pub(super) fn new(title: &str, location: Location, nav: T, flags: SelectFolderDialogFlags) -> Self {
        let mut w = Self {
            base: ModalWindow::new(title, layout!("a:c,w:70,h:20"), window::Flags::Sizeable),
            tv: Handle::None,
            b_ok: Handle::None,
            b_cancel: Handle::None,
            path_viewer: Handle::None,
            nav: nav.clone(),
            path: PathBuf::new(),
            flags,
        };
        w.path = match location {
            Location::Current => nav.current_dir(),
            Location::Last => {
                let m = FOLDER_LAST_PATH.get_or_init(|| Mutex::new(None));
                if let Ok(m) = m.lock() {
                    if let Some(p) = m.as_ref() {
                        p.clone()
                    } else {
                        nav.current_dir()
                    }
                } else {
                    nav.current_dir()
                }
            }
            Location::Path(p) => p.to_path_buf(),
        };
        w.add(label!("&Path,x:1,y:1,w:4"));
        let mut pf = GenericPathFinder::with_navigator(
            w.path.as_path().as_os_str().to_str().unwrap_or(""),
            layout!("l:6,t:1,r:1"),
            pathfinder::Flags::None,
            nav,
        );
        pf.set_hotkey(key!("Alt+P"));
        w.path_viewer = w.add(pf);
        let mut p = panel!("l:1,t:2,r:1,b:3");
        let mut tv = TreeView::with_capacity(
            256,
            layout!("d:f"),
            treeview::Flags::HideHeader
                | treeview::Flags::ScrollBars
                | treeview::Flags::SearchBar
                | treeview::Flags::NoSelection
                | if flags.contains(SelectFolderDialogFlags::Icons) {
                    treeview::Flags::LargeIcons
                } else {
                    treeview::Flags::None
                },
        );
        tv.set_components_toolbar_margins(2, 0);
        tv.sort(0, true);
        w.tv = p.add(tv);
        w.add(p);
        w.b_ok = w.add(button!("&OK,r:1,b:0,w:13"));
        w.b_cancel = w.add(button!("&Cancel,r:15,b:0,w:13"));
        w.set_size_bounds(40, 17, u16::MAX, u16::MAX);
        let h = w.tv;
        w.request_focus_for_control(h);
        w
    }

    fn populate_node(
        &mut self,
        path: &PathBuf,
        parent_node: Handle<treeview::Item<FolderName>>,
        search: &str,
        expand_search: bool,
    ) -> Option<Handle<treeview::Item<FolderName>>> {
        let h = self.tv;
        let entries = self.nav.entries(path);
        let mut result = None;
        let flags = self.flags;
        //log!("SELECT-FOLDER", "Populate Node: Path={:?}, search='{}', entries='{:?}'", path, search, entries);

        if let Some(tv) = self.control_mut(h) {
            tv.add_batch(|tv| {
                for e in entries {
                    if !e.is_container() {
                        continue;
                    }
                    let searched_file = e.name.eq_ignore_ascii_case(search);
                    let mut item = treeview::Item::expandable(FolderName { value: e.name }, if searched_file { !expand_search } else { true });
                    if flags.contains(SelectFolderDialogFlags::Icons) {
                        item.set_icon(['📁', ' ']);
                    }
                    let item_handle = tv.add_item_to_parent(item, parent_node);
                    if searched_file {
                        result = Some(item_handle);
                    }
                }
            });
        }
        result
    }
    fn populate_root(&mut self, search: &str) -> Option<Handle<treeview::Item<FolderName>>> {
        //log!("SELECT-FOLDER", "Populate root with search: '{}'", search);
        let h = self.tv;
        #[cfg(target_family = "unix")]
        let roots = vec![Root {
            path: "/".to_string(),
            name: "Root".to_string(),
            size: 0,
            free_space: 0,
            root_type: fs::RootType::Fixed,
        }];
        #[cfg(not(target_family = "unix"))]
        let roots = self.nav.roots();
        let set_icon = self.flags.contains(SelectFolderDialogFlags::Icons);
        if let Some(tv) = self.control_mut(h) {
            let mut result = None;
            for root in roots {
                let found = (!search.is_empty()) && search[0..1].eq_ignore_ascii_case(&root.path[0..1]);
                let mut item = treeview::Item::expandable(
                    FolderName {
                        value: root.path.to_string(),
                    },
                    !found,
                );
                if set_icon {
                    let icon = root.root_type.icon();
                    item.set_icon([icon, ' ']);
                }
                let handle = tv.add_item(item);
                if found {
                    result = Some(handle);
                }
            }
            result
        } else {
            None
        }
    }
    fn populate_from_path(&mut self) {
        let mut cp = PathBuf::new();
        let current_path = self.path.clone();
        let mut first = true;
        let mut parent_handle = Handle::None;
        let h = self.tv;
        if let Some(tv) = self.control_mut(h) {
            tv.clear()
        }
        //log!("SELECT-FOLDER", "Populate from path: {:?}", current_path);

        let total_components = current_path.components().count();
        for (index, component) in current_path.components().enumerate() {
            if cfg!(target_os = "windows") && component == Component::RootDir {
                continue; // Skip RootDir only  on Windows
            }
            let c = component.as_os_str().to_str().unwrap_or_default();
            if first {
                first = false;
                if let Some(handle) = self.populate_root(c) {
                    parent_handle = handle;
                    cp.push(component);
                    if cfg!(target_os = "windows") {
                        cp.push("\\");
                    }
                } else {
                    break;
                }
            } else if let Some(handle) = self.populate_node(&cp, parent_handle, c, index + 1 < total_components) {
                parent_handle = handle;
                cp.push(component);
            } else {
                break;
            }
        }
        if !parent_handle.is_none() {
            let h = self.tv;
            if let Some(tv) = self.control_mut(h) {
                tv.move_cursor_to(parent_handle);
            }
        }
    }

    fn return_result(&mut self) {
        self.exit_with(FolderSelectionDialogResult::Path(self.path.clone()));
    }
    fn item_to_path(&self, item_handle: Handle<treeview::Item<FolderName>>) -> Option<PathBuf> {
        if let Some(tv) = self.control(self.tv) {
            let mut a: [Handle<treeview::Item<FolderName>>; 256] = [Handle::None; 256];
            let mut pos = 255;
            let mut h = item_handle;
            while let Some(item) = tv.item(h) {
                a[pos] = h;
                h = item.parent().unwrap_or(Handle::None);
                pos -= 1;
                if (pos == 0) || (h.is_none()) {
                    break;
                }
            }
            if pos > 0 {
                let mut path = PathBuf::new();
                for item_handle in a.iter().skip(pos + 1) {
                    if let Some(item) = tv.item(*item_handle) {
                        path.push(item.value().value.as_str());
                    }
                }
                Some(path)
            } else {
                None
            }
        } else {
            None
        }
    }
}
impl<T> ButtonEvents for FolderExplorer<T>
where
    T: Navigator<Entry, Root, PathBuf> + 'static,
{
    fn on_pressed(&mut self, handle: Handle<Button>) -> EventProcessStatus {
        match () {
            _ if handle == self.b_ok => {
                self.return_result();
                EventProcessStatus::Processed
            }
            _ if handle == self.b_cancel => {
                self.exit_with(FolderSelectionDialogResult::Cancel);
                EventProcessStatus::Processed
            }
            _ => EventProcessStatus::Ignored,
        }
    }
}
impl<T> WindowEvents for FolderExplorer<T>
where
    T: Navigator<Entry, Root, PathBuf> + 'static,
{
    fn on_activate(&mut self) {
        log!("SELECT-FOLDER", "Initialing the TreeView");
        self.populate_from_path();
    }
    fn on_accept(&mut self) {
        self.return_result();
    }
    fn on_cancel(&mut self) -> ActionRequest {
        ActionRequest::Allow
    }
}

impl<T> PathFinderEvents for FolderExplorer<T>
where
    T: Navigator<Entry, Root, PathBuf> + 'static,
{
    fn on_path_updated(&mut self, handle: Handle<PathFinder>) -> EventProcessStatus {
        if handle == self.path_viewer {
            if let Some(pv) = self.control(self.path_viewer) {
                self.path = pv.path().to_path_buf();
                self.populate_from_path();
            }
        }
        EventProcessStatus::Processed
    }
}

impl<T> TreeViewEvents<FolderName> for FolderExplorer<T>
where
    T: Navigator<Entry, Root, PathBuf> + 'static,
{
    fn on_current_item_changed(&mut self, _: Handle<TreeView<FolderName>>, item_handle: Handle<treeview::Item<FolderName>>) -> EventProcessStatus {
        if let Some(path) = self.item_to_path(item_handle) {
            let h = self.path_viewer;
            if let Some(pv) = self.control_mut(h) {
                pv.set_path(&path);
            }
            self.path = path;
            EventProcessStatus::Processed
        } else {
            EventProcessStatus::Ignored
        }
    }

    fn on_item_expanded(&mut self, tv: Handle<TreeView<FolderName>>, item_handle: Handle<treeview::Item<FolderName>>, _: bool) -> EventProcessStatus {
        if let Some(tv) = self.control_mut(tv) {
            tv.clear_search();
            tv.delete_item_children(item_handle);
        }
        if let Some(p) = self.item_to_path(item_handle) {
            //log!("INFO", "Item expanded: {:?}, Handle:{:?}", p, item_handle);
            self.populate_node(&p, item_handle, "", false);
        } else {
            //log!("ERROR", "Failed to get path for item {:?}", item_handle);
        }
        EventProcessStatus::Processed
    }

    fn on_item_action(&mut self, _: Handle<TreeView<FolderName>>, _: Handle<treeview::Item<FolderName>>) -> EventProcessStatus {
        self.return_result();
        EventProcessStatus::Processed
    }
}