elio 1.7.0

Snappy, batteries-included terminal file manager with rich previews, inline images, bulk actions, and trash support.
Documentation
use super::{appearance as theme, *};
#[cfg(unix)]
use crate::core::{EntryKind, SymlinkInfo};
use image::ImageFormat;
use ratatui::{style::Modifier, text::Line};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
    fs,
    fs::File,
    io::Write,
    process::Command,
    sync::{Arc, Barrier},
    thread,
};
use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions};

mod archives;
mod audio;
mod binaries;
mod code;
mod data;
mod documents;
mod fonts;
mod helpers;
mod images;
mod markdown;
mod structured;
mod text;
mod videos;

use self::helpers::*;

#[test]
fn truncated_directory_preview_omits_sampled_header_count() {
    let root = temp_path("directory-preview-cap");
    let folder = root.join("folder");
    fs::create_dir_all(&folder).expect("failed to create temp folder");
    let line_limit = default_code_preview_line_limit();
    for index in 0..=line_limit {
        fs::write(folder.join(format!("entry-{index:04}.txt")), "")
            .expect("failed to write directory entry");
    }

    let preview = build_preview(&directory_entry(folder.clone()));

    assert_eq!(preview.kind, PreviewKind::Directory);
    assert_eq!(preview.detail, None);
    assert_eq!(preview.lines.len(), line_limit);
    assert_eq!(
        preview.truncation_note.as_deref(),
        Some(format!("{line_limit} items shown").as_str())
    );

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

#[test]
fn directory_loading_preview_stays_silent() {
    let preview = loading_preview_for(
        &directory_entry(temp_path("directory-loading-preview")),
        &PreviewRequestOptions::Default,
    );

    assert_eq!(preview.kind, PreviewKind::Directory);
    assert_eq!(preview.detail, None);
    assert!(preview.lines.is_empty());
}

#[cfg(unix)]
#[test]
fn directory_preview_marks_symlink_children_and_targets() {
    use std::{os::unix::fs::symlink, path::PathBuf};

    let root = temp_path("directory-preview-symlinks");
    fs::create_dir_all(&root).expect("failed to create temp root");
    fs::create_dir_all(root.join("real-dir")).expect("failed to create real directory");
    fs::write(root.join("target.rs"), "fn main() {}\n").expect("failed to write target");

    let dir_target = PathBuf::from("real-dir");
    let file_target = PathBuf::from("target.rs");
    let missing_target = PathBuf::from("../real/code/missing.rs");
    symlink(&dir_target, root.join("linked-dir")).expect("failed to create directory symlink");
    symlink(&file_target, root.join("linked.rs")).expect("failed to create file symlink");
    symlink(&missing_target, root.join("broken.rs")).expect("failed to create broken symlink");

    let preview = build_preview(&directory_entry(root.clone()));

    let linked_dir_line = preview
        .lines
        .iter()
        .find(|line| line_text(line).contains("linked-dir -> real-dir"))
        .expect("directory preview should show symlinked directory target");
    let linked_file_line = preview
        .lines
        .iter()
        .find(|line| line_text(line).contains("linked.rs -> target.rs"))
        .expect("directory preview should show symlinked file target");
    let broken_line = preview
        .lines
        .iter()
        .find(|line| line_text(line).contains("broken.rs -> ../real/code/missing.rs"))
        .expect("directory preview should show broken symlink target");

    let mut linked_dir = directory_entry(root.join("linked-dir"));
    linked_dir.symlink = Some(SymlinkInfo {
        target: Some(dir_target),
        target_kind: Some(EntryKind::Directory),
    });
    let mut linked_file = file_entry(root.join("linked.rs"));
    linked_file.symlink = Some(SymlinkInfo {
        target: Some(file_target),
        target_kind: Some(EntryKind::File),
    });
    let mut broken = file_entry(root.join("broken.rs"));
    broken.symlink = Some(SymlinkInfo {
        target: Some(missing_target),
        target_kind: None,
    });

    assert_eq!(
        linked_dir_line.spans[0].content.as_ref(),
        format!("{} ", theme::resolve_entry(&linked_dir).icon)
    );
    assert_eq!(
        linked_file_line.spans[0].content.as_ref(),
        format!("{} ", theme::resolve_entry(&linked_file).icon)
    );
    assert_eq!(
        broken_line.spans[0].style.fg,
        Some(theme::resolve_entry(&broken).color),
        "broken symlinks should keep the broken-link preview color"
    );

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

#[cfg(unix)]
#[test]
fn directory_preview_sanitizes_symlink_names_and_targets() {
    use std::{os::unix::fs::symlink, path::PathBuf};

    let root = temp_path("directory-preview-symlink-sanitized");
    fs::create_dir_all(&root).expect("failed to create temp root");
    let target_label = PathBuf::from("bad\rtarget.txt");
    let target = root.join(&target_label);
    let link_name = "bad\u{1b}link.txt";
    fs::write(&target, "hello").expect("failed to write target");
    symlink(&target_label, root.join(link_name)).expect("failed to create symlink");

    let preview = build_preview(&directory_entry(root.clone()));
    let line_texts = preview.lines.iter().map(line_text).collect::<Vec<_>>();

    assert!(
        line_texts
            .iter()
            .any(|line| line.contains("bad^[link.txt -> bad^Mtarget.txt")),
        "expected directory preview to sanitize symlink names and targets, got: {line_texts:?}"
    );
    assert!(line_texts.iter().all(|line| !line.contains('\r')));
    assert!(line_texts.iter().all(|line| !line.contains('\u{1b}')));

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

#[cfg(unix)]
#[test]
fn broken_symlink_preview_reports_target() {
    let root = temp_path("broken-symlink-preview");
    fs::create_dir_all(&root).expect("failed to create temp root");
    let missing = root.join("missing.txt");
    let linked = root.join("linked.txt");
    std::os::unix::fs::symlink(&missing, &linked).expect("failed to create symlink");

    let mut entry = file_entry(linked);
    entry.symlink = Some(SymlinkInfo {
        target: Some(missing.clone()),
        target_kind: None,
    });

    let preview = build_preview(&entry);
    let line_texts = preview.lines.iter().map(line_text).collect::<Vec<_>>();

    assert_eq!(preview.kind, PreviewKind::Unavailable);
    assert_eq!(preview.detail.as_deref(), Some("Broken symlink"));
    assert!(line_texts.iter().any(|line| line == "Broken symbolic link"));
    let missing_label = missing.display().to_string();
    assert!(line_texts.iter().any(|line| line.contains(&missing_label)));

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