dirwalk 1.1.1

Platform-optimized recursive directory walker with metadata
Documentation
use crate::entry::Entry;
use crate::output::DisplayOptions;
use crate::output::format::{format_size_width, write_date, write_name, write_size};
use std::borrow::Borrow;
use std::io::{self, Write};

pub fn write<E: Borrow<Entry>>(
    w: &mut impl Write,
    entries: &[E],
    opts: &DisplayOptions,
) -> io::Result<()> {
    if entries.is_empty() {
        return Ok(());
    }

    let max_size_width = entries
        .iter()
        .map(|e| {
            let entry: &Entry = e.borrow();
            format_size_width(entry.size, entry.is_dir)
        })
        .max()
        .unwrap_or(0);
    let now_secs = time::OffsetDateTime::now_utc().unix_timestamp();

    for e in entries {
        let entry: &Entry = e.borrow();
        // Right-align size: write leading spaces, then size directly (no String allocation).
        let padding = max_size_width - format_size_width(entry.size, entry.is_dir);
        if padding > 0 {
            write!(w, "{:padding$}", "")?;
        }
        write_size(w, entry.size, entry.is_dir)?;
        w.write_all(b"  ")?;
        write_date(w, entry.modified, now_secs)?;
        w.write_all(b"  ")?;
        write_name(w, entry, opts)?;
        writeln!(w)?;
    }

    Ok(())
}

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

    fn make_opts() -> DisplayOptions {
        DisplayOptions {
            short: false,
            classify: true,
            color_enabled: false,
            terminal_width: 80,
            ls_colors: lscolors::LsColors::empty(),
        }
    }

    fn file(name: &str, size: u64, modified: i64) -> Entry {
        Entry {
            relative_path: name.to_owned(),
            depth: 0,
            size,
            is_dir: false,
            is_symlink: false,
            is_hidden: false,
            modified,
        }
    }

    fn dir(name: &str, modified: i64) -> Entry {
        Entry {
            relative_path: name.to_owned(),
            depth: 0,
            size: 0,
            is_dir: true,
            is_symlink: false,
            is_hidden: false,
            modified,
        }
    }

    #[test]
    fn alignment() {
        let entries = vec![
            file("small.txt", 42, 0),
            file("big.bin", 1024 * 1024 * 5, 0),
            dir("src", 0),
        ];
        let refs: Vec<&Entry> = entries.iter().collect();
        let mut buf = Vec::new();
        write(&mut buf, &refs, &make_opts()).unwrap();
        let output = String::from_utf8(buf).unwrap();
        let lines: Vec<&str> = output.lines().collect();

        // All size columns should be right-aligned to the same width.
        // "5.0M" is 4 chars, so "42" and "-" should be padded to 4.
        assert!(lines[0].starts_with("  42"), "got: {:?}", lines[0]);
        assert!(lines[1].starts_with("5.0M"), "got: {:?}", lines[1]);
        assert!(lines[2].starts_with("   -"), "got: {:?}", lines[2]);

        // Classify: dir should have trailing /
        assert!(lines[2].ends_with("src/"), "got: {:?}", lines[2]);
    }

    #[test]
    fn zero_byte_file_shows_zero() {
        let entries = vec![file("empty.lock", 0, 0)];
        let refs: Vec<&Entry> = entries.iter().collect();
        let mut buf = Vec::new();
        write(&mut buf, &refs, &make_opts()).unwrap();
        let output = String::from_utf8(buf).unwrap();
        assert!(
            output.starts_with("0"),
            "expected '0' for empty file, got: {output}"
        );
    }

    #[test]
    fn empty() {
        let mut buf = Vec::new();
        write::<&Entry>(&mut buf, &[], &make_opts()).unwrap();
        assert!(buf.is_empty());
    }
}