colorls 1.0.11

A fast, production-ready Rust rewrite of colorls: a beautified ls with icons, colors, git status and tree view.
use std::io::Write;
use std::sync::Mutex;
use std::time::SystemTime;

use once_cell::sync::Lazy;

/// The name this binary was actually invoked as (`colorls` or `lls`),
/// derived from argv[0]. Both names build from the same binary (see
/// Cargo.toml's two `[[bin]]` entries), so `--help`, `--version`, and
/// every warning/error message adapt to whichever one the user actually
/// ran instead of always saying "colorls".
pub static PROG_NAME: Lazy<String> = Lazy::new(|| {
    std::env::args_os()
        .next()
        .and_then(|p| {
            std::path::Path::new(&p)
                .file_stem()
                .map(|s| s.to_string_lossy().to_string())
        })
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| "colorls".to_string())
});

/// Where `safe_println` writes to. Normally unset (falls back to real
/// stdout directly). When `--paginate`/`-p` spawns a pager, this is set to
/// the pager child process's stdin pipe instead, so every line renders
/// straight into the pager rather than the terminal — see
/// `main::setup_output`. A `Mutex<Option<..>>` (rather than something set
/// permanently once) specifically so `close_output()` can `take()` and
/// drop the writer to close the pipe and send the pager EOF once
/// rendering is done, instead of hanging forever waiting for more input.
static OUTPUT: Lazy<Mutex<Option<Box<dyn Write + Send>>>> = Lazy::new(|| Mutex::new(None));

/// Redirect all subsequent `safe_println` output through `writer` instead
/// of directly to stdout.
pub fn init_output_writer(writer: Box<dyn Write + Send>) {
    *OUTPUT.lock().unwrap() = Some(writer);
}

/// Drop the redirected writer (if any), flushing and closing it. Must be
/// called before waiting on a spawned pager's exit, or it will hang
/// forever waiting for EOF on a pipe we're still holding open.
pub fn close_output() {
    if let Some(mut w) = OUTPUT.lock().unwrap().take() {
        let _ = w.flush();
    }
}

/// Write a line to the current output target (real stdout, or a spawned
/// pager's stdin — see `OUTPUT`), exiting quietly (code 0) on a broken
/// pipe (e.g. `colorls | head`, or a pager the user quit early) instead of
/// panicking the way a bare `println!` would. Any other write failure is
/// reported and exits with a failure code.
pub fn safe_println(s: &str) {
    let mut guard = OUTPUT.lock().unwrap();
    if let Some(w) = guard.as_mut() {
        if let Err(e) = writeln!(w, "{}", s) {
            drop(guard);
            report_write_error(e);
        }
        return;
    }
    drop(guard);

    let stdout = std::io::stdout();
    let mut lock = stdout.lock();
    if let Err(e) = writeln!(lock, "{}", s) {
        report_write_error(e);
    }
}

fn report_write_error(e: std::io::Error) -> ! {
    if e.kind() == std::io::ErrorKind::BrokenPipe {
        std::process::exit(0);
    }
    eprintln!("{}: error writing output: {}", PROG_NAME.as_str(), e);
    std::process::exit(1);
}

#[macro_export]
macro_rules! oprintln {
    () => {
        $crate::util::safe_println("")
    };
    ($($arg:tt)*) => {
        $crate::util::safe_println(&format!($($arg)*))
    };
}

use chrono::{DateTime, Local};
use unicode_width::UnicodeWidthStr;

/// Format a byte count the way `ls -lh` / colorls do: one decimal place
/// past the first unit, no decimal for bytes.
pub fn human_size(bytes: u64) -> String {
    const UNITS: [&str; 6] = ["B", "K", "M", "G", "T", "P"];
    if bytes == 0 {
        return "0B".to_string();
    }
    let mut size = bytes as f64;
    let mut unit_idx = 0;
    while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
        size /= 1024.0;
        unit_idx += 1;
    }
    if unit_idx == 0 {
        format!("{}{}", bytes, UNITS[0])
    } else {
        format!("{:.1}{}", size, UNITS[unit_idx])
    }
}

pub fn format_time(t: SystemTime) -> String {
    let dt: DateTime<Local> = t.into();
    dt.format("%d %b %H:%M").to_string()
}
/// Detect terminal width, falling back to 80 columns when not attached to a
/// TTY (pipes, redirected output, CI, etc.) or when detection fails.
pub fn terminal_width() -> usize {
    terminal_size::terminal_size()
        .map(|(w, _)| w.0 as usize)
        .filter(|w| *w > 0)
        .unwrap_or(80)
}

/// Visible column width of a string, accounting for wide (CJK) characters.
/// Nerd-font icon glyphs are typically reported as width 1 or 2 depending
/// on the font; we treat the whole private-use-area icon range as width 2
/// to match how most nerd fonts actually render them in a terminal grid.
pub fn display_width(s: &str) -> usize {
    s.chars()
        .map(|c| {
            let cp = c as u32;
            if (0xE000..=0xF8FF).contains(&cp) || (0xF0000..=0xFFFFD).contains(&cp) {
                2
            } else {
                UnicodeWidthStr::width(c.to_string().as_str())
            }
        })
        .sum()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn human_size_bytes() {
        assert_eq!(human_size(0), "0B");
        assert_eq!(human_size(512), "512B");
        assert_eq!(human_size(1023), "1023B");
    }

    #[test]
    fn human_size_kilobytes_and_up() {
        assert_eq!(human_size(1024), "1.0K");
        assert_eq!(human_size(1536), "1.5K");
        assert_eq!(human_size(1024 * 1024), "1.0M");
        assert_eq!(human_size(1024 * 1024 * 1024), "1.0G");
    }

    #[test]
    fn display_width_ascii() {
        assert_eq!(display_width("hello"), 5);
        assert_eq!(display_width(""), 0);
    }

    #[test]
    fn display_width_icon_glyph_counts_as_two() {
        // A Nerd Font private-use-area glyph should be treated as width 2,
        // matching how these fonts actually render in a terminal grid.
        let icon = "\u{f15b}";
        assert_eq!(display_width(icon), 2);
    }
}