lla 0.3.8

A lightweight ls replacement
use crate::commands::args::Args;
use crate::theme::{color_value_to_color, get_file_color, is_no_color, ColorValue, Theme};
use colored::*;
use std::path::Path;
use std::sync::OnceLock;

static CURRENT_THEME: OnceLock<Theme> = OnceLock::new();

pub fn set_theme(theme: Theme) {
    let _ = CURRENT_THEME.set(theme);
}

pub fn get_theme() -> &'static Theme {
    CURRENT_THEME.get_or_init(Theme::default)
}

fn get_color(color_value: &ColorValue) -> Color {
    color_value_to_color(color_value)
}

pub fn colorize_file_name(path: &Path) -> ColoredString {
    let name = path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or_else(|| path.to_str().unwrap_or(""));

    if is_no_color() {
        return if path.is_dir() {
            format!("{}/", name).normal()
        } else {
            name.normal()
        };
    }

    let theme = get_theme();

    if path.is_dir() {
        if let Some(color) = get_file_color(path) {
            format!("{}/", name).color(color).bold()
        } else {
            name.to_string()
                .color(get_color(&theme.colors.directory))
                .bold()
        }
    } else if path.is_symlink() {
        name.color(get_color(&theme.colors.symlink))
            .italic()
            .underline()
    } else if is_executable(path) {
        name.color(get_color(&theme.colors.executable)).bold()
    } else if let Some(color) = get_extension_color(path) {
        name.color(color)
    } else {
        name.color(get_color(&theme.colors.file))
    }
}

pub fn colorize_file_name_with_icon(path: &Path, content: String) -> ColoredString {
    let parts: Vec<&str> = content.split(' ').collect();
    if parts.len() != 2 {
        return if is_no_color() {
            content.normal()
        } else {
            content.color(get_color(&get_theme().colors.file))
        };
    }

    let icon = parts[0];
    let name = parts[1];

    if is_no_color() {
        return if path.is_dir() {
            format!("{} {}", icon, name).normal()
        } else {
            format!("{} {}", icon, name).normal()
        };
    }

    let theme = get_theme();

    if path.is_dir() {
        if let Some(color) = get_file_color(path) {
            format!("{} {}", icon, name).color(color).bold()
        } else {
            format!("{} {}", icon, name)
                .color(get_color(&theme.colors.directory))
                .bold()
        }
    } else if path.is_symlink() {
        format!("{} {}", icon, name)
            .color(get_color(&theme.colors.symlink))
            .italic()
            .underline()
    } else if is_executable(path) {
        format!("{} {}", icon, name)
            .color(get_color(&theme.colors.executable))
            .bold()
    } else if let Some(color) = get_extension_color(path) {
        format!("{} {}", icon, name).color(color)
    } else {
        format!("{} {}", icon, name).color(get_color(&theme.colors.file))
    }
}

pub fn colorize_size(size: u64) -> ColoredString {
    let formatted = if size < 1024 {
        format!("{}B", size)
    } else if size < 1024 * 1024 {
        format!("{:.1}K", size as f64 / 1024.0)
    } else if size < 1024 * 1024 * 1024 {
        format!("{:.1}M", size as f64 / (1024.0 * 1024.0))
    } else {
        format!("{:.1}G", size as f64 / (1024.0 * 1024.0 * 1024.0))
    };

    if is_no_color() {
        formatted.normal()
    } else {
        let theme = get_theme();
        formatted.color(get_color(&theme.colors.size))
    }
}

pub fn colorize_group(group: &str) -> ColoredString {
    if is_no_color() {
        group.normal()
    } else {
        group.color(get_color(&get_theme().colors.group))
    }
}

pub fn colorize_user(user: &str) -> ColoredString {
    if is_no_color() {
        user.normal()
    } else {
        user.color(get_color(&get_theme().colors.user))
    }
}

use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;

pub fn colorize_permissions(permissions: &Permissions) -> String {
    let mode = permissions.mode();

    if is_no_color() {
        return format_permissions_no_color(mode);
    }

    let theme = get_theme();

    let file_type = if mode & 0o40000 != 0 {
        "d".color(get_color(&theme.colors.permission_dir))
    } else {
        "-".color(get_color(&theme.colors.permission_none))
    };
    let user = triplet(mode, 6);
    let group = triplet(mode, 3);
    let other = triplet(mode, 0);
    format!("{}{}{}{}", file_type, user, group, other)
}

fn format_permissions_no_color(mode: u32) -> String {
    let file_type = if mode & 0o40000u32 != 0u32 { "d" } else { "-" };
    let read = |shift| {
        if mode >> shift & 0o4u32 != 0u32 {
            "r"
        } else {
            "-"
        }
    };
    let write = |shift| {
        if mode >> shift & 0o2u32 != 0u32 {
            "w"
        } else {
            "-"
        }
    };
    let exec = |shift| {
        if mode >> shift & 0o1u32 != 0u32 {
            "x"
        } else {
            "-"
        }
    };

    format!(
        "{}{}{}{}{}{}{}{}{}{}",
        file_type,
        read(6),
        write(6),
        exec(6),
        read(3),
        write(3),
        exec(3),
        read(0),
        write(0),
        exec(0)
    )
}

fn triplet(mode: u32, shift: u32) -> String {
    let theme = get_theme();
    let r = if mode >> (shift + 2) & 1u32 != 0 {
        "r".color(get_color(&theme.colors.permission_read))
            .to_string()
    } else {
        "-".color(get_color(&theme.colors.permission_none))
            .to_string()
    };
    let w = if mode >> (shift + 1) & 1u32 != 0 {
        "w".color(get_color(&theme.colors.permission_write))
            .to_string()
    } else {
        "-".color(get_color(&theme.colors.permission_none))
            .to_string()
    };
    let x = if mode >> shift & 1u32 != 0 {
        "x".color(get_color(&theme.colors.permission_exec))
            .to_string()
    } else {
        "-".color(get_color(&theme.colors.permission_none))
            .to_string()
    };
    format!("{}{}{}", r, w, x)
}

pub fn colorize_date(date: &std::time::SystemTime) -> ColoredString {
    let datetime: chrono::DateTime<chrono::Local> = (*date).into();
    let formatted = datetime.format("%b %d %H:%M").to_string();

    if is_no_color() {
        formatted.normal()
    } else {
        formatted.color(get_color(&get_theme().colors.date))
    }
}

fn is_executable(path: &Path) -> bool {
    #[cfg(unix)]
    {
        if let Ok(metadata) = path.metadata() {
            return metadata.permissions().mode() & 0o111 != 0;
        }
    }
    false
}

fn get_extension_color(path: &Path) -> Option<Color> {
    if is_no_color() {
        return None;
    }
    get_file_color(path)
}

pub struct ColorState {
    pub no_color: bool,
}

impl ColorState {
    pub fn new(args: &Args) -> Self {
        Self {
            no_color: args.no_color,
        }
    }

    pub fn is_enabled(&self) -> bool {
        !self.no_color
    }
}