runa-tui 0.5.2

A fast, keyboard-focused terminal file browser (TUI). Highly configurable and lightweight. Previously known as runner-tui.
Documentation
//! File and directory browsing logic for runa.
//!
//! Provides the FileEntry struct which is used throughout runa.
//! Also holds all the FileInfo and FileType structs used by the ShowInfo Overlay

use crate::core::format_attributes;

use std::borrow::Cow;
use std::ffi::OsString;
use std::fs::{self, symlink_metadata};
use std::io;
use std::path::Path;
use std::time::SystemTime;

/// Represents a single entry in a directory listing
/// Holds the name, display name, and attributes like is_dir, is_hidden, is_system
/// Used throughout runa for directory browsing and file management
/// Created and populated by the browse_dir function.
///
/// # Fields
/// * `name` - The original OsString name of the file or directory
/// * `lowercase_name` - The lowercase version of the name for case-insensitive comparisons
/// * `flags` - A u8 bitfield representing attributes (is_dir, is_hidden, is_system, is_symlink)
#[derive(Debug, Clone)]
pub struct FileEntry {
    name: OsString,
    lowercase_name: Box<str>,
    flags: u8,
}

impl FileEntry {
    // Flag bit definitions
    // These are used to set and check attributes in the flags field
    const IS_DIR: u8 = 1 << 0;
    const IS_HIDDEN: u8 = 1 << 1;
    const IS_SYSTEM: u8 = 1 << 2;
    const IS_SYMLINK: u8 = 1 << 3;

    fn new(name: OsString, flags: u8) -> Self {
        let lowercase_name = name.to_string_lossy().to_lowercase().into_boxed_str();
        FileEntry {
            name,
            lowercase_name,
            flags,
        }
    }

    // Accessors

    pub fn name(&self) -> &OsString {
        &self.name
    }

    pub fn name_str(&self) -> Cow<'_, str> {
        self.name.to_string_lossy()
    }

    pub fn lowercase_name(&self) -> &str {
        &self.lowercase_name
    }

    pub fn is_dir(&self) -> bool {
        self.flags & Self::IS_DIR != 0
    }

    pub fn is_hidden(&self) -> bool {
        self.flags & Self::IS_HIDDEN != 0
    }

    pub fn is_system(&self) -> bool {
        self.flags & Self::IS_SYSTEM != 0
    }

    pub fn is_symlink(&self) -> bool {
        self.flags & Self::IS_SYMLINK != 0
    }

    /// Returns the file extension in lowercase if it exists
    /// # Returns
    /// An Option containing the lowercase file extension as a String, or None if no extension exists
    pub fn extension(&self) -> Option<String> {
        Path::new(&self.name)
            .extension()
            .and_then(|s| s.to_str())
            .map(|s| s.to_ascii_lowercase())
    }
}

/// Enumerator for the filye types which are then shown inside [FileInfo]
///
/// Hold File, Directory, Symlink and Other types.
#[derive(Debug, Clone, PartialEq)]
pub enum FileType {
    File,
    Directory,
    Symlink,
    Other,
}

/// Main FileInfo struct that holds each info field for the ShowInfo overlay widget.
/// Holds name, size, modified time, attributes string, and file type.
///
/// # Fields
/// * `name` - The OsString name of the file or directory
/// * `size` - The size of the file in bytes (None for directories)
/// * `modified` - The last modified time as SystemTime (None if unavailable)
/// * `attributes` - A formatted string of file attributes
/// * `file_type` - The FileType enum indicating if it's a file, directory, symlink, or other
#[derive(Debug, Clone, PartialEq)]
pub struct FileInfo {
    name: OsString,
    size: Option<u64>,
    modified: Option<SystemTime>,
    attributes: String,
    file_type: FileType,
}

impl FileInfo {
    // Accessors

    pub fn name(&self) -> &OsString {
        &self.name
    }

    pub fn size(&self) -> &Option<u64> {
        &self.size
    }

    pub fn modified(&self) -> &Option<SystemTime> {
        &self.modified
    }

    pub fn attributes(&self) -> &str {
        &self.attributes
    }

    pub fn file_type(&self) -> &FileType {
        &self.file_type
    }

    /// Main file info getter used by the ShowInfo overlay functions
    ///
    /// # Arguments
    /// * `path` - Path reference to the file or directory to get info for
    ///
    /// # Returns
    /// A FileInfo struct populated with the file's information.
    pub fn get_file_info(path: &Path) -> io::Result<FileInfo> {
        let metadata = symlink_metadata(path)?;
        let file_type = if metadata.is_file() {
            FileType::File
        } else if metadata.is_dir() {
            FileType::Directory
        } else if metadata.file_type().is_symlink() {
            FileType::Symlink
        } else {
            FileType::Other
        };

        Ok(FileInfo {
            name: path.file_name().unwrap_or_default().to_os_string(),
            size: if metadata.is_file() {
                Some(metadata.len())
            } else {
                None
            },
            modified: metadata.modified().ok(),
            attributes: format_attributes(&metadata),
            file_type,
        })
    }
}

/// Reads the cotents of the proviced directory and returns them in a vector of FileEntry
///
/// # Arguments
/// * `path` - Path reference to the directory to browse
///
/// # Returns
/// A Result containing a vector of FileEntry structs or an std::io::Error
pub fn browse_dir(path: &Path) -> io::Result<Vec<FileEntry>> {
    let mut entries = Vec::with_capacity(256);

    for entry in fs::read_dir(path)? {
        let entry = match entry {
            Ok(e) => e,
            Err(_) => continue,
        };

        let name = entry.file_name();
        let ft = match entry.file_type() {
            Ok(ft) => ft,
            Err(_) => continue,
        };

        let mut flags = 0u8;
        if ft.is_dir() {
            flags |= FileEntry::IS_DIR;
        }
        if ft.is_symlink() {
            flags |= FileEntry::IS_SYMLINK;
        }

        #[cfg(unix)]
        {
            use std::os::unix::ffi::OsStrExt;
            if name.as_bytes().first() == Some(&b'.') {
                flags |= FileEntry::IS_HIDDEN;
            }
        }

        #[cfg(windows)]
        {
            use std::os::windows::fs::MetadataExt;
            if let Ok(md) = entry.metadata() {
                let attrs = md.file_attributes();

                if attrs & 0x2 != 0 {
                    flags |= FileEntry::IS_HIDDEN;
                }
                if attrs & 0x4 != 0 {
                    flags |= FileEntry::IS_SYSTEM;
                }
            }

            if flags & FileEntry::IS_HIDDEN == 0 && name.to_string_lossy().starts_with('.') {
                flags |= FileEntry::IS_HIDDEN;
            }
        }

        entries.push(FileEntry::new(name, flags));
    }
    Ok(entries)
}