use crate::entry::Entry;
use crate::output::DisplayOptions;
use crate::output::format::{name_width, write_name};
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 term_w = opts.terminal_width as usize;
let widths: Vec<usize> = entries
.iter()
.map(|e| name_width(e.borrow(), opts.classify))
.collect();
let max_name = widths.iter().copied().max().unwrap_or(0);
let col_width = max_name + 2;
let num_cols = if max_name >= term_w {
1
} else {
(term_w - max_name) / col_width + 1
};
let num_rows = entries.len().div_ceil(num_cols);
for row in 0..num_rows {
for col in 0..num_cols {
let idx = col * num_rows + row;
if idx >= entries.len() {
break;
}
let entry: &Entry = entries[idx].borrow();
let is_last_col = col == num_cols - 1 || (col + 1) * num_rows + row >= entries.len();
write_name(w, entry, opts)?;
if !is_last_col {
let padding = col_width.saturating_sub(widths[idx]);
write!(w, "{:width$}", "", width = padding)?;
}
}
writeln!(w)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_opts(terminal_width: u16, classify: bool) -> DisplayOptions {
DisplayOptions {
short: true,
classify,
color_enabled: false,
terminal_width,
ls_colors: lscolors::LsColors::empty(),
}
}
fn file(name: &str) -> Entry {
Entry {
relative_path: name.to_owned(),
depth: 0,
size: 1,
is_dir: false,
is_symlink: false,
is_hidden: false,
modified: 0,
}
}
fn dir(name: &str) -> Entry {
Entry {
relative_path: name.to_owned(),
depth: 0,
size: 0,
is_dir: true,
is_symlink: false,
is_hidden: false,
modified: 0,
}
}
#[test]
fn single_column_narrow_terminal() {
let entries = vec![file("aaaa"), file("bbbb"), file("cccc")];
let refs: Vec<&Entry> = entries.iter().collect();
let mut buf = Vec::new();
write(&mut buf, &refs, &make_opts(8, false)).unwrap();
let output = String::from_utf8(buf).unwrap();
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "aaaa");
assert_eq!(lines[1], "bbbb");
assert_eq!(lines[2], "cccc");
}
#[test]
fn multi_column() {
let entries: Vec<Entry> = (0..7).map(|i| file(&format!("f{i:03}"))).collect();
let refs: Vec<&Entry> = entries.iter().collect();
let mut buf = Vec::new();
write(&mut buf, &refs, &make_opts(20, false)).unwrap();
let output = String::from_utf8(buf).unwrap();
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 3);
assert!(lines[0].contains("f000"), "got: {:?}", lines[0]);
assert!(lines[0].contains("f003"), "got: {:?}", lines[0]);
assert!(lines[0].contains("f006"), "got: {:?}", lines[0]);
}
#[test]
fn last_column_no_padding() {
let entries: Vec<Entry> = (0..5).map(|i| file(&format!("f{i:03}"))).collect();
let refs: Vec<&Entry> = entries.iter().collect();
let mut buf = Vec::new();
write(&mut buf, &refs, &make_opts(10, false)).unwrap();
let output = String::from_utf8(buf).unwrap();
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 3, "expected 3 rows, got: {output}");
assert!(lines[0].contains("f000"), "got: {:?}", lines[0]);
assert!(lines[0].contains("f003"), "got: {:?}", lines[0]);
}
#[test]
fn classify_adds_suffix() {
let entries = vec![dir("src"), file("main.rs")];
let refs: Vec<&Entry> = entries.iter().collect();
let mut buf = Vec::new();
write(&mut buf, &refs, &make_opts(80, true)).unwrap();
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("src/"), "got: {output}");
assert!(output.contains("main.rs"), "got: {output}");
assert!(!output.contains("main.rs/"), "got: {output}");
}
#[test]
fn empty() {
let mut buf = Vec::new();
write::<&Entry>(&mut buf, &[], &make_opts(80, false)).unwrap();
assert!(buf.is_empty());
}
}