df3 0.1.2

Advanced disk free utility - modern alternative to df command
Documentation
// File: src\output.rs
// Author: Hadi Cahyadi <cumulus13@gmail.com>
// Date: 2026-06-16
// Description:
// License: MIT

use crate::cli::{Cli, SortField};
use crate::config::Config;
use crate::disk::DiskInfo;
use anyhow::Result;
use colored::*;
use std::io;

pub use crate::cli::OutputFormat;

pub fn display_table(disk_info: &DiskInfo, cli: &Cli, config: &Config) {
    let mut entries = disk_info.entries.clone();

    // Sort if requested
    if let Some(sort_field) = &cli.sort {
        entries.sort_by(|a, b| {
            let cmp = match sort_field {
                SortField::Filesystem => a.filesystem.cmp(&b.filesystem),
                SortField::Size => a.total.cmp(&b.total),
                SortField::Used => a.used.cmp(&b.used),
                SortField::Avail => a.available.cmp(&b.available),
                SortField::Percent => a.percent.partial_cmp(&b.percent).unwrap(),
                SortField::Mounted => a.mount_point.cmp(&b.mount_point),
            };
            if cli.reverse {
                cmp.reverse()
            } else {
                cmp
            }
        });
    }

    // Calculate dynamic widths based on content - add padding for safety
    let fs_width = entries
        .iter()
        .map(|e| e.filesystem.len())
        .max()
        .unwrap_or(10)
        .max(10)
        .min(30)
        + 2; // Add padding

    let mount_width = entries
        .iter()
        .map(|e| e.mount_point.display().to_string().len())
        .max()
        .unwrap_or(10)
        .max(10)
        .min(40)
        + 2; // Add padding

    let size_width = entries
        .iter()
        .map(|e| format_size(e.total, cli.human_readable).len())
        .max()
        .unwrap_or(10)
        .max(10)
        + 2;

    let used_width = entries
        .iter()
        .map(|e| format_size(e.used, cli.human_readable).len())
        .max()
        .unwrap_or(10)
        .max(10)
        + 2;

    let avail_width = entries
        .iter()
        .map(|e| format_size(e.available, cli.human_readable).len())
        .max()
        .unwrap_or(10)
        .max(10)
        + 2;

    // Fixed widths
    let type_width = 6;
    let percent_width = 5;

    // Calculate total line width exactly
    // Format: "│ " + fs + " │ " + type + " │ " + size + " │ " + used + " │ " + avail + " │ " + percent + " │ " + mount + " │"
    let _total_width = 1
        + 1
        + fs_width
        + 1
        + 1
        + type_width
        + 1
        + 1
        + size_width
        + 1
        + 1
        + used_width
        + 1
        + 1
        + avail_width
        + 1
        + 1
        + percent_width
        + 1
        + 1
        + mount_width
        + 1
        + 1;
    // Simplified: 2 + fs_width + 3 + type_width + 3 + size_width + 3 + used_width + 3 + avail_width + 3 + percent_width + 3 + mount_width + 2
    let total_width = 2
        + fs_width
        + 3
        + type_width
        + 3
        + size_width
        + 3
        + used_width
        + 3
        + avail_width
        + 3
        + percent_width
        + 3
        + mount_width
        + 2;

    // Print top border
    let border_color = hex_to_rgb(&config.colors.border_color);
    let top_border = format!("{}", "".repeat(total_width - 2));
    println!(
        "{}",
        top_border.truecolor(border_color.0, border_color.1, border_color.2)
    );

    // Print header
    let header_color = hex_to_rgb(&config.colors.header_color);
    let header_line = format!(
        "│ {:<fs$} │ {:<tp$} │ {:>sz$} │ {:>us$} │ {:>av$} │ {:>pc$} │ {:<mt$} │",
        "Filesystem",
        "Type",
        "Size",
        "Used",
        "Avail",
        "Use%",
        "Mounted on",
        fs = fs_width,
        tp = type_width,
        sz = size_width,
        us = used_width,
        av = avail_width,
        pc = percent_width,
        mt = mount_width
    );
    println!(
        "{}",
        header_line.truecolor(header_color.0, header_color.1, header_color.2)
    );

    // Print separator
    let sep_line = format!("{}", "".repeat(total_width - 2));
    println!(
        "{}",
        sep_line.truecolor(border_color.0, border_color.1, border_color.2)
    );

    // Print data rows
    for entry in &entries {
        let fs_color = hex_to_rgb(&config.colors.filesystem_color);
        let mount_color = hex_to_rgb(&config.colors.mount_color);
        let avail_color = hex_to_rgb(&config.colors.avail_color);

        // Color code based on usage percentage
        let usage_color = if entry.percent >= 90.0 {
            hex_to_rgb(&config.colors.high_usage_color)
        } else if entry.percent >= 75.0 {
            hex_to_rgb(&config.colors.warning_usage_color)
        } else {
            hex_to_rgb(&config.colors.normal_usage_color)
        };

        let percent_str = format!("{:.0}%", entry.percent);
        let fs_display = entry.filesystem.clone();
        let mount_display = entry.mount_point.display().to_string();
        let size_display = format_size(entry.total, cli.human_readable);
        let used_display = format_size(entry.used, cli.human_readable);
        let avail_display = format_size(entry.available, cli.human_readable);

        // Build the line in parts to ensure proper alignment
        print!("");
        print!(
            "{}",
            format!("{:<fs$}", fs_display, fs = fs_width)
                .truecolor(fs_color.0, fs_color.1, fs_color.2)
        );
        print!("");
        print!("{:<tp$}", entry.disk_type, tp = type_width);
        print!("");
        print!("{:>sz$}", size_display, sz = size_width);
        print!("");
        print!("{:>us$}", used_display, us = used_width);
        print!("");
        print!(
            "{}",
            format!("{:>av$}", avail_display, av = avail_width).truecolor(
                avail_color.0,
                avail_color.1,
                avail_color.2
            )
        );
        print!("");
        print!(
            "{}",
            format!("{:>pc$}", percent_str, pc = percent_width).truecolor(
                usage_color.0,
                usage_color.1,
                usage_color.2
            )
        );
        print!("");
        print!(
            "{}",
            format!("{:<mt$}", mount_display, mt = mount_width).truecolor(
                mount_color.0,
                mount_color.1,
                mount_color.2
            )
        );
        println!("");
    }

    // Print bottom border
    let bottom_border = format!("{}", "".repeat(total_width - 2));
    println!(
        "{}",
        bottom_border.truecolor(border_color.0, border_color.1, border_color.2)
    );

    // Show total if requested
    if cli.total {
        if let Some(total) = &disk_info.total {
            let total_size = format_size(total.total, cli.human_readable);
            let total_used = format_size(total.used, cli.human_readable);
            let total_avail = format_size(total.available, cli.human_readable);

            println!();
            let total_sep = "".repeat(total_width - 2).to_string();
            println!(
                "{}",
                total_sep.truecolor(border_color.0, border_color.1, border_color.2)
            );
            println!("{}", "Totals:".bold().truecolor(0, 255, 255));
            println!("  Total Size:  {}", total_size);
            println!("  Total Used:  {}", total_used);
            println!("  Total Avail: {}", total_avail);
        }
    }

    // Print timestamp
    let timestamp = format!(
        "Last updated: {}",
        disk_info.collected_at.format("%Y-%m-%d %H:%M:%S")
    );
    println!();
    println!("{}", timestamp.truecolor(128, 128, 128));
}

fn hex_to_rgb(hex: &str) -> (u8, u8, u8) {
    let hex = hex.trim_start_matches('#');
    if hex.len() != 6 {
        return (255, 255, 255);
    }
    let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255);
    let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(255);
    let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255);
    (r, g, b)
}

pub fn display_json(disk_info: &DiskInfo) -> Result<()> {
    let json = serde_json::to_string_pretty(&disk_info.entries)?;
    println!("{}", json);
    Ok(())
}

pub fn display_csv(disk_info: &DiskInfo) -> Result<()> {
    let mut wtr = csv::Writer::from_writer(io::stdout());
    for entry in &disk_info.entries {
        wtr.serialize(entry)?;
    }
    wtr.flush()?;
    Ok(())
}

pub fn display_inodes(disk_info: &DiskInfo, cli: &Cli, _config: &Config) {
    for entry in &disk_info.entries {
        println!(
            "{:<20} {:>10} {:>10} {:>10} {:>6.0}% {:<20}",
            entry.filesystem,
            format_size(entry.total, cli.human_readable),
            format_size(entry.used, cli.human_readable),
            format_size(entry.available, cli.human_readable),
            entry.percent,
            entry.mount_point.display()
        );
    }
}

pub async fn watch_mode(
    interval: u64,
    human_readable: bool,
    local_only: bool,
    all: bool,
    fs_type: Vec<String>,
    exclude_type: Vec<String>,
    block_size: u64,
    config: Config,
) -> Result<()> {
    let mut tick = tokio::time::interval(tokio::time::Duration::from_secs(interval));

    loop {
        tick.tick().await;
        print!("\x1B[2J\x1B[1;1H"); // Clear screen

        let disk_info = crate::disk::DiskInfo::collect(
            human_readable,
            local_only,
            all,
            &fs_type,
            &exclude_type,
            block_size,
        )?;

        display_table(
            &disk_info,
            &Cli {
                human_readable,
                local_only,
                all,
                inodes: false,
                output: OutputFormat::Table,
                block_size,
                fs_type: fs_type.clone(),
                exclude_type: exclude_type.clone(),
                sort: None,
                reverse: false,
                total: true,
                sync: false,
                watch: 0,
                version: false,
                mounts: vec![],
                config: None,
            },
            &config,
        );
    }
}

fn format_size(size: u64, human_readable: bool) -> String {
    if human_readable {
        humansize::format_size(size, humansize::BINARY)
    } else {
        size.to_string()
    }
}