eza 0.23.4

A modern replacement for ls
Documentation
// SPDX-FileCopyrightText: 2024 Christina Sørensen
// SPDX-License-Identifier: EUPL-1.2
//
// SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors
// SPDX-FileCopyrightText: 2014 Benjamin Sago
// SPDX-License-Identifier: MIT
use log::trace;
use nu_ansi_term::{Color as Colour, Style};
use palette::{FromColor, LinSrgb, Oklab, Srgb};

use crate::{
    fs::{dir_action::RecurseOptions, feature::git::GitCache, fields::Size, DotFilter, File},
    output::{table::TimeType, tree::TreeDepth},
};

#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct ColorScaleOptions {
    pub mode: ColorScaleMode,
    pub min_luminance: isize,
    pub size: bool,
    pub age: bool,
}

impl Default for ColorScaleOptions {
    fn default() -> Self {
        Self {
            mode: ColorScaleMode::Fixed,
            min_luminance: 50,
            size: false,
            age: false,
        }
    }
}

#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum ColorScaleMode {
    Fixed,
    Gradient,
}

#[derive(Copy, Clone, Debug)]
pub struct ColorScaleInformation {
    pub options: ColorScaleOptions,

    pub accessed: Option<Extremes>,
    pub changed: Option<Extremes>,
    pub created: Option<Extremes>,
    pub modified: Option<Extremes>,

    pub size: Option<Extremes>,
}

impl ColorScaleInformation {
    pub fn from_color_scale(
        color_scale: ColorScaleOptions,
        files: &[File<'_>],
        dot_filter: DotFilter,
        git: Option<&GitCache>,
        git_ignoring: bool,
        r: Option<RecurseOptions>,
    ) -> Option<Self> {
        if color_scale.mode == ColorScaleMode::Fixed {
            None
        } else {
            let mut information = Self {
                options: color_scale,
                accessed: None,
                changed: None,
                created: None,
                modified: None,
                size: None,
            };

            update_information_recursively(
                &mut information,
                files,
                dot_filter,
                git,
                git_ignoring,
                TreeDepth::root(),
                r,
            );

            Some(information)
        }
    }

    #[must_use]
    pub fn adjust_style(&self, mut style: Style, value: f32, range: Option<Extremes>) -> Style {
        if let (Some(fg), Some(range)) = (style.foreground, range) {
            let mut ratio = ((value - range.min) / (range.max - range.min)).clamp(0.0, 1.0);
            if ratio.is_nan() {
                ratio = 1.0;
            }

            style.foreground = Some(adjust_luminance(
                fg,
                ratio,
                self.options.min_luminance as f32 / 100.0,
            ));
        }

        style
    }

    pub fn apply_time_gradient(&self, style: Style, file: &File<'_>, time_type: TimeType) -> Style {
        let range = match time_type {
            TimeType::Modified => self.modified,
            TimeType::Changed => self.changed,
            TimeType::Accessed => self.accessed,
            TimeType::Created => self.created,
        };

        if let Some(file_time) = time_type.get_corresponding_time(file) {
            self.adjust_style(style, file_time.and_utc().timestamp_millis() as f32, range)
        } else {
            style
        }
    }
}

fn update_information_recursively(
    information: &mut ColorScaleInformation,
    files: &[File<'_>],
    dot_filter: DotFilter,
    git: Option<&GitCache>,
    git_ignoring: bool,
    depth: TreeDepth,
    r: Option<RecurseOptions>,
) {
    for file in files {
        if information.options.age {
            Extremes::update(
                file.created_time()
                    .map(|x| x.and_utc().timestamp_millis() as f32),
                &mut information.created,
            );
            Extremes::update(
                file.modified_time()
                    .map(|x| x.and_utc().timestamp_millis() as f32),
                &mut information.modified,
            );
            Extremes::update(
                file.accessed_time()
                    .map(|x| x.and_utc().timestamp_millis() as f32),
                &mut information.accessed,
            );
            Extremes::update(
                file.changed_time()
                    .map(|x| x.and_utc().timestamp_millis() as f32),
                &mut information.changed,
            );
        }

        if information.options.size {
            let size = match file.size() {
                Size::Some(size) => Some(size as f32),
                _ => None,
            };
            Extremes::update(size, &mut information.size);
        }

        // We don't want to recurse into . and .., but still want to list them, therefore bypass
        // the dot_filter.
        if file.is_directory()
            && r.is_some_and(|x| !x.is_too_deep(depth.0))
            && file.name != "."
            && file.name != ".."
        {
            match file.read_dir() {
                Ok(dir) => {
                    let files: Vec<File<'_>> = dir
                        .files(dot_filter, git, git_ignoring, false, false)
                        .collect();

                    update_information_recursively(
                        information,
                        &files,
                        dot_filter,
                        git,
                        git_ignoring,
                        depth.deeper(),
                        r,
                    );
                }
                Err(e) => trace!("Unable to access directory {}: {}", file.name, e),
            }
        }
    }
}

#[derive(Copy, Clone, Debug)]
pub struct Extremes {
    max: f32,
    min: f32,
}

impl Extremes {
    fn update(maybe_value: Option<f32>, maybe_range: &mut Option<Extremes>) {
        match (maybe_value, maybe_range) {
            (Some(value), Some(range)) => {
                if value > range.max {
                    range.max = value;
                } else if value < range.min {
                    range.min = value;
                }
            }
            (Some(value), rel) => {
                let _ = rel.insert({
                    Extremes {
                        max: value,
                        min: value,
                    }
                });
            }
            _ => (),
        }
    }
}

fn adjust_luminance(color: Colour, x: f32, min_l: f32) -> Colour {
    let rgb_color = match color {
        Colour::Rgb(r, g, b) => LinSrgb::new(
            f32::from(r) / 255.0,
            f32::from(g) / 255.0,
            f32::from(b) / 255.0,
        ),

        Colour::Black => LinSrgb::new(0.0, 0.0, 0.0),

        Colour::Green | Colour::LightGreen => LinSrgb::new(0.0, 1.0, 0.0),

        Colour::Yellow | Colour::LightYellow => LinSrgb::new(1.0, 1.0, 0.0),

        Colour::Blue | Colour::LightBlue => LinSrgb::new(0.0, 0.0, 1.0),

        Colour::Magenta | Colour::LightMagenta => LinSrgb::new(1.0, 0.0, 1.0),

        Colour::Cyan | Colour::LightCyan => LinSrgb::new(0.0, 1.0, 1.0),

        Colour::White => LinSrgb::new(1.0, 1.0, 1.0),

        Colour::LightGray => LinSrgb::new(0.5, 0.5, 0.5),

        Colour::LightRed | Colour::Red => LinSrgb::new(1.0, 0.0, 0.0),

        Colour::DarkGray => LinSrgb::new(0.25, 0.25, 0.25),

        Colour::LightPurple | Colour::Purple => LinSrgb::new(0.5, 0.0, 0.5),

        _ => LinSrgb::new(1.0, 1.0, 1.0),
    };

    let mut lab: Oklab = Oklab::from_color(rgb_color);
    lab.l = (min_l + (1.0 - min_l) * (-4.0 * (1.0 - x)).exp()).clamp(0.0, 1.0);

    let adjusted_rgb: Srgb<f32> = Srgb::from_color(lab);
    Colour::Rgb(
        (adjusted_rgb.red * 255.0).round() as u8,
        (adjusted_rgb.green * 255.0).round() as u8,
        (adjusted_rgb.blue * 255.0).round() as u8,
    )
}