lla 0.3.7

A lightweight ls replacement
use super::FileFormatter;
use crate::error::Result;
use crate::plugin::PluginManager;
use crate::theme::{self, ColorValue};
use crate::utils::color::{self, *};
use crate::utils::icons::format_with_icon;
use colored::*;
use lla_plugin_interface::proto::DecoratedEntry;
use std::cmp;
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::time::{Duration, UNIX_EPOCH};
use unicode_width::UnicodeWidthStr;

pub struct TableFormatter {
    pub show_icons: bool,
}

impl TableFormatter {
    pub fn new(show_icons: bool) -> Self {
        Self { show_icons }
    }
}
impl TableFormatter {
    const PADDING: usize = 1;

    fn strip_ansi(s: &str) -> String {
        String::from_utf8(strip_ansi_escapes::strip(s).unwrap_or_default()).unwrap_or_default()
    }

    fn visible_width(s: &str) -> usize {
        Self::strip_ansi(s).width()
    }

    fn calculate_column_widths(files: &[DecoratedEntry]) -> [usize; 4] {
        let mut widths = [
            "Permissions".len(),
            "Size".len(),
            "Modified".len(),
            "Name".len(),
        ];

        for entry in files {
            let metadata = entry.metadata.as_ref().cloned().unwrap_or_default();
            let perms = Permissions::from_mode(metadata.permissions);
            let perms = colorize_permissions(&perms);
            widths[0] = cmp::max(widths[0], Self::visible_width(&perms));

            let size: ColoredString = colorize_size(metadata.size);
            widths[1] = cmp::max(widths[1], Self::visible_width(&size));

            let modified = UNIX_EPOCH + Duration::from_secs(metadata.modified);
            let date = colorize_date(&modified);
            widths[2] = cmp::max(widths[2], Self::visible_width(&date));

            let path = Path::new(&entry.path);
            let colored_name = colorize_file_name(path).to_string();
            let name_with_icon =
                colorize_file_name_with_icon(path, format_with_icon(path, colored_name, true));
            widths[3] = cmp::max(widths[3], Self::visible_width(&name_with_icon));
        }

        widths
    }

    fn get_border_color() -> Color {
        theme::color_value_to_color(&ColorValue::Named("bright black".to_string()))
    }

    fn get_header_color() -> Color {
        let theme = color::get_theme();
        theme::color_value_to_color(&theme.colors.directory)
    }

    fn create_separator(widths: &[usize]) -> String {
        let border_color = Self::get_border_color();
        let mut separator = String::new();
        separator.push('');
        for (i, &width) in widths.iter().enumerate() {
            separator.push_str(&"".repeat(width + Self::PADDING * 2));
            if i < widths.len() - 1 {
                separator.push('');
            }
        }
        separator.push('');
        separator.color(border_color).to_string()
    }

    fn create_header(widths: &[usize]) -> String {
        let border_color = Self::get_border_color();
        let header_color = Self::get_header_color();
        let headers = ["Permissions", "Size", "Modified", "Name"];
        let mut header = String::new();
        header.push('');

        for (&width, &title) in widths.iter().zip(headers.iter()) {
            header.push(' ');
            header.push_str(
                &format!("{:width$}", title, width = width)
                    .color(header_color)
                    .bold()
                    .to_string(),
            );
            header.push(' ');
            header.push('');
        }
        header.color(border_color).to_string()
    }

    fn create_top_border(widths: &[usize]) -> String {
        let border_color = Self::get_border_color();
        let mut border = String::new();
        border.push('');
        for (i, &width) in widths.iter().enumerate() {
            border.push_str(&"".repeat(width + Self::PADDING * 2));
            if i < widths.len() - 1 {
                border.push('');
            }
        }
        border.push('');
        border.color(border_color).to_string()
    }

    fn create_bottom_border(widths: &[usize]) -> String {
        let border_color = Self::get_border_color();
        let mut border = String::new();
        border.push('');
        for (i, &width) in widths.iter().enumerate() {
            border.push_str(&"".repeat(width + Self::PADDING * 2));
            if i < widths.len() - 1 {
                border.push('');
            }
        }
        border.push('');
        border.color(border_color).to_string()
    }

    fn format_cell(content: &str, width: usize, align_right: bool) -> String {
        let visible_width = Self::visible_width(content);
        let padding = width.saturating_sub(visible_width);

        if align_right {
            format!("{}{}", " ".repeat(padding), content)
        } else {
            format!("{}{}", content, " ".repeat(padding))
        }
    }
}

impl FileFormatter for TableFormatter {
    fn format_files(
        &self,
        files: &[lla_plugin_interface::proto::DecoratedEntry],
        plugin_manager: &mut PluginManager,
        _depth: Option<usize>,
    ) -> Result<String> {
        if files.is_empty() {
            return Ok(String::new());
        }

        let widths = Self::calculate_column_widths(files);

        let mut output = String::new();
        output.push_str(&Self::create_top_border(&widths));
        output.push('\n');
        output.push_str(&Self::create_header(&widths));
        output.push('\n');
        output.push_str(&Self::create_separator(&widths));
        output.push('\n');

        for entry in files {
            let metadata = entry.metadata.as_ref().cloned().unwrap_or_default();
            let perms = Permissions::from_mode(metadata.permissions);
            let perms = colorize_permissions(&perms);
            let size = colorize_size(metadata.size);
            let modified = UNIX_EPOCH + Duration::from_secs(metadata.modified);
            let date = colorize_date(&modified);
            let path = Path::new(&entry.path);
            let colored_name = colorize_file_name(path).to_string();
            let name = format_with_icon(path, colored_name, self.show_icons);

            let plugin_fields = plugin_manager.format_fields(entry, "table").join(" ");
            let plugin_suffix = if plugin_fields.is_empty() {
                String::new()
            } else {
                format!(" {}", plugin_fields)
            };

            let formatted_perms = Self::format_cell(&perms, widths[0], false);
            let formatted_size = Self::format_cell(&size, widths[1], true);
            let formatted_date = Self::format_cell(&date, widths[2], false);
            let formatted_name = Self::format_cell(&name, widths[3], false);

            output.push_str(&format!(
                "{pad}{}{pad}{pad}{}{pad}{pad}{}{pad}{pad}{}{pad}{}\n",
                formatted_perms,
                formatted_size,
                formatted_date,
                formatted_name,
                plugin_suffix,
                pad = " ",
            ));
        }

        output.push_str(&Self::create_bottom_border(&widths));
        output.push('\n');

        Ok(output)
    }
}