elio 1.3.0

Snappy, batteries-included terminal file manager with rich previews, inline images, bulk actions, and trash support.
Documentation
use super::resolution::{PlaceResolutionContext, build_sidebar_rows_with_context};
use crate::{
    config::{BuiltinPlace, PlaceEntrySpec, PlacesConfig},
    core::{SidebarItemKind, SidebarRow},
};
use std::{
    fs,
    path::{Path, PathBuf},
    time::{SystemTime, UNIX_EPOCH},
};

fn temp_path(label: &str) -> PathBuf {
    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system time should be after unix epoch")
        .as_nanos();
    std::env::temp_dir().join(format!("elio-places-{label}-{unique}"))
}

fn context_for(root: &Path) -> PlaceResolutionContext {
    let home = root.join("home");
    let downloads = home.join("Downloads");
    let trash = root.join("trash");
    fs::create_dir_all(&downloads).expect("failed to create downloads");
    fs::create_dir_all(&trash).expect("failed to create trash");
    PlaceResolutionContext {
        home,
        desktop: None,
        documents: None,
        downloads: Some(downloads),
        pictures: None,
        music: None,
        videos: None,
        root: None,
        trash: Some(trash),
    }
}

#[test]
fn configured_places_order_and_semantic_kinds_are_preserved() {
    let root = temp_path("ordered-sidebar");
    let context = context_for(&root);
    let projects = root.join("projects");
    let places = PlacesConfig {
        show_devices: false,
        entries: vec![
            PlaceEntrySpec::Builtin {
                place: BuiltinPlace::Downloads,
                icon: Some("D".to_string()),
            },
            PlaceEntrySpec::Custom {
                title: "Projects".to_string(),
                path: projects.clone(),
                icon: Some("P".to_string()),
            },
            PlaceEntrySpec::Builtin {
                place: BuiltinPlace::Home,
                icon: None,
            },
            PlaceEntrySpec::Builtin {
                place: BuiltinPlace::Trash,
                icon: None,
            },
        ],
    };

    let rows = build_sidebar_rows_with_context(&places, &context);
    let items = rows.iter().filter_map(SidebarRow::item).collect::<Vec<_>>();

    assert_eq!(items.len(), 4);
    assert_eq!(items[0].title, "Downloads");
    assert_eq!(items[0].kind, SidebarItemKind::Downloads);
    assert_eq!(items[0].icon, "D");
    assert_eq!(items[1].title, "Projects");
    assert_eq!(items[1].kind, SidebarItemKind::Custom);
    assert_eq!(items[1].icon, "P");
    assert_eq!(items[1].path, projects);
    assert_eq!(items[2].title, "Home");
    assert_eq!(items[2].kind, SidebarItemKind::Home);
    assert_eq!(items[3].title, "Trash");
    assert_eq!(items[3].kind, SidebarItemKind::Trash);
    assert!(rows.iter().all(|row| matches!(row, SidebarRow::Item(_))));

    fs::remove_dir_all(root).expect("failed to remove temp root");
}

#[test]
fn missing_builtin_places_are_skipped_but_nonexistent_custom_places_stay_visible() {
    let root = temp_path("missing-builtins");
    let context = context_for(&root);
    let future_mount = root.join("mnt").join("camera");
    let places = PlacesConfig {
        show_devices: false,
        entries: vec![
            PlaceEntrySpec::Builtin {
                place: BuiltinPlace::Desktop,
                icon: None,
            },
            PlaceEntrySpec::Custom {
                title: "Camera".to_string(),
                path: future_mount.clone(),
                icon: None,
            },
            PlaceEntrySpec::Builtin {
                place: BuiltinPlace::Downloads,
                icon: None,
            },
        ],
    };

    let rows = build_sidebar_rows_with_context(&places, &context);
    let items = rows.iter().filter_map(SidebarRow::item).collect::<Vec<_>>();

    assert_eq!(items.len(), 2);
    assert_eq!(items[0].title, "Camera");
    assert_eq!(items[0].kind, SidebarItemKind::Custom);
    assert_eq!(items[0].path, future_mount);
    assert_eq!(items[1].title, "Downloads");

    fs::remove_dir_all(root).expect("failed to remove temp root");
}

#[test]
fn localized_builtin_places_show_resolved_folder_name() {
    let root = temp_path("localized-builtins");
    let home = root.join("home");
    let downloads = home.join("Descargas");
    fs::create_dir_all(&downloads).expect("failed to create downloads");
    let context = PlaceResolutionContext {
        home,
        desktop: None,
        documents: None,
        downloads: Some(downloads.clone()),
        pictures: None,
        music: None,
        videos: None,
        root: None,
        trash: None,
    };
    let places = PlacesConfig {
        show_devices: false,
        entries: vec![PlaceEntrySpec::Builtin {
            place: BuiltinPlace::Downloads,
            icon: None,
        }],
    };

    let rows = build_sidebar_rows_with_context(&places, &context);
    let items = rows.iter().filter_map(SidebarRow::item).collect::<Vec<_>>();

    assert_eq!(items.len(), 1);
    assert_eq!(items[0].title, "Descargas");
    assert_eq!(items[0].kind, SidebarItemKind::Downloads);
    assert_eq!(items[0].path, downloads);

    fs::remove_dir_all(root).expect("failed to remove temp root");
}

#[test]
fn places_deduplicate_entries_by_resolved_path() {
    let root = temp_path("dedupe-sidebar");
    let context = context_for(&root);
    let places = PlacesConfig {
        show_devices: false,
        entries: vec![
            PlaceEntrySpec::Builtin {
                place: BuiltinPlace::Home,
                icon: None,
            },
            PlaceEntrySpec::Custom {
                title: "Home 2".to_string(),
                path: context.home.clone(),
                icon: Some("H".to_string()),
            },
            PlaceEntrySpec::Builtin {
                place: BuiltinPlace::Downloads,
                icon: None,
            },
            PlaceEntrySpec::Custom {
                title: "Downloads Alias".to_string(),
                path: context.home.join("Downloads").join("..").join("Downloads"),
                icon: Some("A".to_string()),
            },
        ],
    };

    let rows = build_sidebar_rows_with_context(&places, &context);
    let items = rows.iter().filter_map(SidebarRow::item).collect::<Vec<_>>();

    assert_eq!(items.len(), 2);
    assert_eq!(items[0].title, "Home");
    assert_eq!(items[1].title, "Downloads");

    fs::remove_dir_all(root).expect("failed to remove temp root");
}